Skip to main content
Identity verification connects the web widget to the accounts on your own site or app. When someone is signed in there, your backend signs a short-lived token that says who they are. The widget hands that token to Keloa, and Keloa checks the signature on every widget request. On this page, a user is someone with an account on your site or app. In Keloa, each user becomes a contact. Your team then sees the user’s verified details in the Inbox, the user’s conversations follow them to every device, and your AI agent can safely look up their data.

Why use it

Without identity verification, everything the widget knows about a visitor comes from the visitor. Anyone can type someone else’s email address into a conversation, and a visitor ID in a browser proves nothing about who is typing. Keloa’s AI therefore never uses a contact’s email address to look up customer data on an anonymous website conversation, whatever address the contact has. The store sections in your team’s inbox are different: see What your team sees. With identity verification:
  • Nobody can chat as one of your users. Keloa only accepts identity that your backend signed with a secret only you and Keloa know. There is no unsigned way to identify a visitor.
  • Your team knows who they’re talking to. The context drawer shows the user ID, the profile and any extra attributes your site sends.
  • Conversations follow the user. A signed-in user sees their own conversations in the widget on every browser and device.
  • The AI can use the verified email. Tools that look up contact data by email (Shopify orders, and TRONVoice invoices, quotes and subscriptions) use it on that user’s own conversations, and a TRONVoice link your team made from that user’s conversation also counts for email written from that verified address.

How it works

  1. A user signs in to your site or app.
  2. Your server creates a token for that user: a JSON Web Token (JWT) with their user ID and profile, signed with HS256 and your secret.
  3. Your page passes the token to the widget with Keloa('identify', { getToken }): a function that fetches it from your own endpoint. The token is never part of the page’s HTML.
  4. The widget sends the token with every request. Keloa verifies the signature, the expiry and the audience, then links the conversation to that user’s contact.
  5. When the token expires or Keloa rejects it, the widget calls getToken() for a fresh one.
  6. When the user signs out, your page calls Keloa('logout'), the widget forgets them in that browser, and your other open tabs reload.
Identity verification is set per AI agent. Each agent has its own embed snippet, its own secrets and its own setting.

Before you start

  • A backend with signed-in users. The token must be signed on your server. A static site or a page without accounts can’t use identity verification.
  • HTTPS. With identity verification on, only https pages of your allowed sites, on the standard https port (443), can show the widget. For development, localhost, 127.0.0.1, *.test and *.localhost also work over http and on any port.
  • The right role. Only workspace owners and admins can create or revoke secrets and change the mode. See Members & roles.
  • The widget installed. Your site already loads the embed snippet. See Web widget.

Set it up in Keloa

1

Open the agent's web widget settings

Go to AI agents, open the agent whose widget runs on your site, and select the Channels tab. The Web widget card holds both Allowed origins and Identity verification.
2

Add your site under Allowed origins

Under Allowed origins, click Add origin and enter each host that shows the widget, without https://, a port or a path. Changes save automatically.
  • app.acme.com matches exactly that host. acme.com does not cover www.acme.com; add both if you need both.
  • *.acme.com matches every subdomain of acme.com, but not acme.com itself.
  • Only add a wildcard for a domain you control. Keloa refuses wildcards over public suffixes (from the Public Suffix List) and over known shared-hosting platforms, such as *.com, *.co.uk, *.vercel.app, *.github.io or *.myshopify.com. Keloa can’t know every hosting platform, so a wildcard it accepts isn’t proof that you control every site under it. For a hosted site, add its exact address (acme.vercel.app).
  • A host typed with non-ASCII letters is saved in its punycode form, which is what browsers send: *.bücher.de becomes *.xn--bcher-kva.de.
Identity verification needs at least one allowed origin. Once it’s on, only these sites can show the widget, and you can’t empty the list until you turn verification Off again.
3

Create a secret

Under Identity verification, click Create secret. Keloa shows two values once:
  • Key ID (kid) — public. It goes in the token header so Keloa knows which secret you used.
  • Secret — private. It starts with kis_. Your server uses it, exactly as shown, as the HMAC key.
Keloa shows the secret only once. Copy it, store it in your server’s environment or secrets manager, then click I’ve stored it. If you lose it, revoke it and create a new one. Never put the secret in a web page, a mobile app, a front-end bundle or a code repository.
4

Sign tokens and pass them to the widget

Add the token signing, the token endpoint and the page code from the sections below. Under How to set it up, Keloa shows short versions of these samples that already contain your agent ID and key ID.All samples call the token endpoint with POST, protect it with your framework’s CSRF check and return the token as JSON ({"token": "…"}). If your endpoint returns another format, make sure getToken() reads that format and resolves to the token string.
5

Choose who can chat

Under Signed-in users, pick a mode:The Optional and Required buttons stay unavailable until the agent has a secret and at least one allowed origin. Keloa asks for confirmation before it switches to Required or back to Off.

Sign the token on your server

The token is a compact JWT signed with HMAC-SHA256 (HS256). Every sample below does the same thing:
  1. Build the header: alg HS256, typ JWT and kid set to your key ID.
  2. Build the claims: user_id, aud (your agent ID), iat, exp and the profile fields you want to share.
  3. Encode the header and the claims as JSON, then base64url without padding (no trailing =).
  4. Sign <header>.<claims> with your secret and append the base64url signature.
Your agent ID is the agent value in your embed snippet’s loader URL. Keep the secret, key ID and agent ID in configuration, never in code. Give tokens a short lifetime: 15 to 60 minutes is a good range, and Keloa refuses anything over 24 hours. Never let a token outlive the user’s session on your site.
Use the secret as a plain string, including its kis_ prefix. Don’t hex-decode or base64-decode it before signing.

When users change their email

email_verified must be false for an address the user hasn’t confirmed, including a new address right after they change it. The Laravel sample above signs true only when your User model implements Illuminate\Contracts\Auth\MustVerifyEmail and email_verified_at is set, so it is only correct if your app clears email_verified_at whenever the email changes. Whether it does depends on your own code:
  • Breeze’s ProfileController and the current Laravel starter kits clear it on every email change.
  • Fortify’s and Jetstream’s UpdateUserProfileInformation action only clears it when your User implements MustVerifyEmail. Jetstream’s default User doesn’t.
  • Custom profile forms, admin tools, imports, seeders and social sign-in code can set email_verified_at or leave it in place.
Check your own flow: change a test user’s email and make sure email_verified_at is empty until they click the confirmation link sent to the new address. In any framework, also require the user’s current password to change their email. Otherwise anyone at a signed-in browser can switch the account to an address they control, confirm it and take the account over, including the user’s conversations in the widget.

Add a token endpoint

The widget gets every token from this endpoint: once when a signed-in page loads, and again whenever the current token expires or Keloa rejects it. Give it an endpoint on your own site that:
  • only answers a signed-in session, and answers 401 (not a redirect to your login page) otherwise;
  • accepts POST with your framework’s CSRF protection, so only your own pages can call it;
  • sends no CORS headers, and stays out of any CORS configuration that reflects the request’s origin with credentials;
  • is rate limited, for example to 30 requests per minute per user;
  • returns the token as JSON ({"token": "…"}) with Cache-Control: no-store, private;
  • doesn’t keep an idle session alive: while a signed-in chat is open, the widget calls it in the background whenever its token expires, so these calls must not count as user activity or extend the session, and the endpoint answers 401 once the user has been idle longer than your timeout.
This endpoint is the only place a token ever leaves your server: never render one into a page.
Idle timeouts. Most frameworks renew the session on every request, this endpoint’s included, so a tab with the chat open would keep an idle user signed in. If you sign users out after a period of inactivity, keep that timer yourself: record the time of the user’s last request in the session on every route except this endpoint, and have this endpoint end the session and answer 401 once that time is older than your timeout. In Laravel, also keep this route from saving the session back (its StartSession renews the stored session and its cookie), so a refresh never moves the session’s own expiry either.

Hand the token to the widget

Put this on every page where a user is signed in. It can go before or after your embed snippet: the first line is the same small stub your snippet starts with, so Keloa(…) works before the widget has loaded.
Adjust where the CSRF value comes from to your framework. Laravel and Rails read it from the csrf-token meta tag, as above (header names are case-insensitive, so X-CSRF-Token works for both). Django reads it from the csrftoken cookie and expects it in the X-CSRFToken header.
Never render a token into the page. Pass getToken only, so the token only ever travels in your token endpoint’s response. Browsers and frameworks bring back earlier pages without asking your server: the back/forward cache, the copies Livewire wire:navigate, Turbo Drive and other single-page navigation keep in memory, a disk cache, a CDN or a full-page cache. A token written into such a page would sign the widget in again on Back, even after the user signed out. With getToken alone, a restored page asks your endpoint, which answers 401 (or a CSRF error) once the session is gone, and the widget ends the signed-in session.

When the user signs out

Call Keloa('logout') whenever the user signs out. The widget stops showing the signed-in user’s conversations, clears what it kept in that browser and starts a new anonymous visitor. The conversations stay open in your inbox. Keloa('logout') only acts in the browser tab that calls it, and it doesn’t invalidate tokens on Keloa’s side: a token your backend already issued stays valid until its exp. So also tell your other open tabs, and let them reload. A reloaded tab is rendered by your server as signed out, so it shows neither the widget’s signed-in conversations nor your own app’s private pages. Put this on every page, signed in or not, and call keloaSignOut() when the user signs out:
Where to call keloaSignOut():
  • Sign-out with a full page load (a form POST and a redirect): call it on the page the user lands on. Flash a value on the redirect, for example keloa_logout, and only call it when that value is present.
  • Single-page apps (React, Vue, Inertia, Livewire wire:navigate, Turbo): always call it when the user signs out, right after your sign-out request succeeds. The widget lives on across page swaps, so nothing else tells it. Do this even when your sign-out also loads a new page.
Each open tab is also covered when its token runs out: once the session is gone, getToken() gets 401 and the widget ends the signed-in session. Keep tokens short-lived to bound what a tab that missed the message can do, and revoke the secret if you need to cut off every token at once (see Rotate or revoke a secret).

Laravel Blade partial

A complete partial for a Laravel layout, built on the KeloaIdentityToken class and the endpoint above. It:
  • identifies signed-in users with getToken alone, so no token is ever part of the HTML;
  • signs the widget out when the user signs out, and reloads every other open tab that showed a signed-in page;
  • reloads a page the browser restores from its back/forward cache after it identified someone;
  • loads the widget.
It renders nothing until KELOA_AGENT_ID and KELOA_LOADER_URL are set, and the widget stays anonymous if signing fails. The script runs again on every Livewire wire:navigate or Turbo page swap, which is how it notices that the session is gone.
Flash the sign-out for the next page, so the partial calls Keloa('logout') there and reloads your other tabs:
A Livewire wire:navigate sign-out that never flashes is covered too: the partial runs again on the next page, sees that the session is gone and signs out everywhere. An Inertia app renders the partial once, in its root view, and never runs it again on page visits, so call window.__keloaSignOut() from your sign-out handler, right after the sign-out request succeeds.
The browser can still show earlier pages of your own app on Back after a sign-out, from its back/forward cache or from a single-page framework’s memory. That doesn’t sign the widget back in: the restored page only has getToken, which gets 401. To keep your own pages out of the browser’s disk cache, send Cache-Control: no-store, private on pages for signed-in users.

Claims reference

Claims

The whole token can be at most 8,192 characters. A missing or invalid required claim rejects the token. Keloa trims and cleans the optional profile claims (email, name, first_name, last_name, phone, language, company and the attributes):
  • Text that is too long is cut to the limit: a name, company or attribute string is shortened, not dropped.
  • A value that is invalid, such as an email that isn’t a valid address or is longer than 255 characters, or a phone in the wrong format, is dropped and never stored. The rest of the token still counts.
  • Control characters and invisible formatting characters in text are replaced with a space, and repeated spaces are collapsed.
A few optional claims reject the whole token when they’re invalid: an nbf that isn’t a number, a typ other than JWT (both malformed) and an aud without this agent’s ID (wrong_audience). Claims nested more than 8 levels deep, for example deeply nested attribute values, also reject the token as malformed.
Send email_verified: true only for an address the user proved they own, for example by clicking a confirmation link. If you send true for an address nobody confirmed, anyone who signs up on your site with someone else’s email can join that person’s existing contact in Keloa, and get their Shopify orders and TRONVoice invoices through the AI. Send false for an address from a social or OAuth sign-in that didn’t confirm it, for an address a teammate or admin typed in, and for a changed address until the user confirms it.

How Keloa uses the claims

  • Contact. The first time Keloa sees a user, it joins an existing contact only when email_verified is true and the address exactly matches a contact email Keloa already trusts, and that contact doesn’t already belong to another signed-in user or to an anonymous website visitor. Otherwise it creates a new contact. Your team can still merge the two contacts later.
  • Profile sync. On later visits, Keloa fills empty fields and updates a field when your site’s value changed since the last sync. A change a teammate made stays until your site sends a different value.
  • Email. A verified address becomes the contact’s email when the contact has none. It also replaces an address the visitor typed into the conversation (which Keloa never trusted), unless another signed-in user shares the contact, and it replaces the address this user’s own token verified before when your site sends a new verified one. Keloa never replaces an address your team entered or one that arrived by email. An address sent without email_verified: true only fills an empty email, is not trusted and never replaces an address.
  • Company. A company needs a name. With an id, users with the same id share one company that Keloa created for that ID from this agent. With only a name, Keloa creates a company for that user alone. Keloa never joins a company your team created, or any existing company by name.
  • Attributes. They appear in the Verified user block in the inbox. A key that matches the key of a contact custom field fills that field. null never clears a value. When several signed-in users share a contact, the AI and macros on a website conversation only use a synced field when that conversation’s own user sent that value.
Only send attributes your team may see. Never send passwords, tokens, payment or bank details, or other sensitive data.

JavaScript API reference

Every call goes through the Keloa function. Calls made before the widget loads are queued by the stub and run in order once it loads.

identify options

getToken() must resolve to the token string. When it rejects, resolves to nothing or to an object, returns text that isn’t a token (for example your login page), or takes longer than 10 seconds, the widget ends the signed-in session. The widget calls it at most 3 times per minute; if Keloa keeps rejecting the tokens it returns, the widget stops asking and warns in the browser console.
The widget never puts the token in a URL or in browser storage, and only sends it to Keloa’s own origin.

Security model

What Keloa guarantees

  • Only signed identity counts. Keloa accepts identity only from a token signed with one of the agent’s active secrets. There is no unsigned identify call; Keloa('identify') without a token does nothing.
  • Every request is checked. The token travels with every widget request, and Keloa verifies the signature, expiry and audience each time.
  • A rejected token is never downgraded. A token that fails verification is never treated as an anonymous visitor. Keloa answers 401 with a reason code and the widget asks your page for a fresh token.
  • One agent, one set of users. Secrets belong to one agent, and Keloa keys each user on the agent plus the user ID. A token for one agent can never speak for another agent’s users.
  • Conversations stay with their owner. A signed-in user’s conversations are only visible to that user. Anonymous visitors can never read them, and an anonymous visitor only reaches the conversations they started in that browser, even after a teammate merges contacts. The one exception is a website conversation from before this release, when Keloa didn’t yet record which browser started a conversation: it is open to every visitor on its contact, so after a contact merge another visitor’s browser can open it, until your team merges it with a newer conversation of the visitor who wrote it, which ties it to that visitor (see Identity matching and merging). A website conversation only merges with conversations of the same visitor or the same signed-in user: Keloa refuses to merge it with anyone else’s conversation or with another channel’s, so another person’s messages, subject and tags never reach it. A website conversation that a merge made before that rule mixed with another person’s or another channel’s conversation is never shown in the widget again, and the AI, emails and flows leave out what Keloa can’t attribute to the visitor (see Website conversations merged before the rule).
  • Anonymous visitors are kept apart per workspace. The widget gives each browser a separate random visitor ID for every Keloa workspace, also when several shops share one host (for example shop-a.example-host.com and shop-b.example-host.com). Keloa never shows that ID to your team: the contact card says Anonymous website visitor, and the inbox and data exports only hold a keyed code that opens nothing. So no team can use what it sees to open a visitor’s conversation with another workspace. Visitors who chatted before this change start a new anonymous conversation once; their earlier conversations stay in your inbox. The ID they used before is retired for good, in every workspace: a browser tab that still runs the widget from before the change can’t start or open a conversation with it, and chats again once the page reloads.
  • Only your sites can show the widget. With verification on, Keloa tells browsers that only your allowed origins, over https on the standard port, may embed the widget. Another site can’t frame it to push its own user’s token into a visitor’s conversation.
  • Secrets are protected. Secrets are stored encrypted and shown only once. Only owners and admins can create or revoke them, and every change is recorded in the audit log. Keloa never logs tokens. Revoking a secret cuts off every token signed with it at once, including live-update connections that are already open (see Rotate or revoke a secret).
  • Contact data needs a verified email. AI tools only use an email your site verified on that same user’s conversations. On an anonymous website conversation the AI never uses the contact’s email address for customer data: the Shopify order lookup asks the visitor for the email on the order, and TRONVoice data only comes through a link a teammate made from that same conversation. See Tools.
  • Anonymous conversations only use what the visitor said there. On an anonymous website conversation, the AI, flow messages, conditions and action values, macros and the widget’s own messages only use what the visitor said in that conversation, or in their own earlier conversations your team merged into it: a name, email address or phone number they typed in the chat or the contact form. {{contact.email}} and {{contact.phone}} render empty unless the visitor typed that address or number there. A name, company or language that your team, an email or a merge put on the contact, and every contact and company custom field, your team’s included, count as unknown; the language follows the visitor’s own messages. On a signed-in user’s conversation, the name, company, language and custom fields are the ones your site signed for that user, and the email and phone also show the verified email and the phone your site signed. An AI-drafted email on an anonymous conversation only greets the visitor by a name they gave there. On a conversation from before this release that is still open to every visitor on its contact (see above), what was typed there shows to whoever opens it.
  • Email replies stay with the visitor. Your team can answer a website conversation by email only at an address shown to be the visitor’s: one they typed in that conversation (in a message or the contact form) or in the contact form of one of their own earlier conversations your team merged into it, the verified email your site signed for that signed-in user, an address whose owner already emailed into that conversation, or an address your team corrected on a contact that wasn’t merged with another contact. Otherwise the reply goes to the chat. An email sent from the conversation only quotes and carries the visitor’s own messages and subject, and only names the visitor by a name they gave. See Replying to conversations. See Messages & actions and Conditions.

What your site must guarantee

Keloa trusts what your backend signs. Keep that trust deserved:
  • Sign only for authenticated users. Create tokens on your server for the user of the current session, never for an ID the browser sends you.
  • Don’t sign for impersonated users. When your staff use a “log in as user” or impersonation feature, don’t sign a token for the impersonated user. Otherwise their messages are stored as that user’s conversations, follow the user to their devices, and unlock the user’s data through the AI.
  • Keep the secret secret. Store it in your environment or secrets manager. Never ship it to a browser, a mobile app or a repository. Revoke it at once if it leaks.
  • Use a stable user ID. Use an ID that never changes and is never reused for another person. Don’t use the email address as the user ID.
  • Be honest about email_verified. Send true only when the user proved they own the address, for example with a confirmation link. If users can change their email, send false until they confirm the new address, and require their current password for the change (see When users change their email). A wrong true lets someone take over another person’s contact and data (see the warning under Claims reference).
  • Keep tokens short-lived. A token is a bearer credential: anyone who holds it can chat as that user until it expires, and a live-update connection opened with it keeps receiving that conversation’s new replies for up to 13 minutes after its exp (the 60-second clock-skew allowance, plus up to 12 minutes until the connection’s channel moves on). Use 15–60 minutes and getToken to refresh, and never let a token outlive the user’s session on your site.
  • Serve your site over HTTPS.
  • Protect the token endpoint. Require a session, use POST with CSRF protection, send no CORS headers, rate-limit it and answer 401 when signed out.
  • Watch the scripts on signed-in pages. Any script on those pages can call your token endpoint and read the token. Keep third-party tags off them where you can, and protect them against XSS, for example with a Content Security Policy.
  • Never put a token in a page. Pass getToken only, never token, so a page restored from the back/forward cache, a single-page framework’s memory, a CDN or a full-page cache can’t sign the widget in again. Send Cache-Control: no-store, private on the token endpoint, so no cache ever keeps its answer.
  • Keep tokens out of URLs, storage and logs. Don’t put them in query strings, localStorage, analytics or error reports.
  • Call Keloa('logout') on every sign-out, also in single-page apps, and reload your other open tabs (see When the user signs out). Logout only clears the browser tab that calls it; it doesn’t invalidate tokens you already issued.
The widget handles pages the browser restores from its back/forward cache: it asks getToken() again and never resumes a signed-in conversation from the restored page. Reloading the page on pageshow when event.persisted is true is still a good idea if the page itself shows private data.

Rotate or revoke a secret

Each agent can have at most 2 active secrets, so you can rotate without signing anyone out. The second secret exists for rotation only: both secrets sign for the same users, so never use it for another environment (see Staging and development).
1

Create a second secret

Click Create secret and store the new secret and key ID on your server.
2

Deploy the new secret

Switch your backend to sign with the new secret and its kid. Tokens signed with the old secret keep working meanwhile.
3

Wait for old tokens to expire

Wait at least your token lifetime. The secrets list shows when each secret was last used; it updates at most every 5 minutes, so it can lag by up to 5 minutes.
4

Revoke the old secret

Click Revoke next to the old secret and confirm.
Revoking takes effect immediately: every token signed with that secret stops working at once, for sending messages, loading conversations and live updates. Live-update connections already opened with those tokens stop receiving your team’s replies, closed notices and satisfaction surveys at once too. A token that simply expires works the same way, with a short delay: a live-update connection opened with it stops receiving within 13 minutes of the token’s exp. Keloa accepts a token for 60 seconds after exp (clock skew), and a connection keeps its channel for at most 12 minutes after it was opened. Signed-in users whose widget is open when you revoke keep chatting once your site hands the widget a token signed with the new secret. Live replies can pause for up to about 10 minutes until the widget renews its connection; they then appear from the conversation history. If you revoke the last secret:
  • in Optional mode, signed-in users chat anonymously until you create a new secret and update your site;
  • in Required mode, nobody can chat until you do.
If a secret leaks, revoke it right away, then create a new one and deploy it. Don’t wait for a rotation window.

Staging and development

Use a separate AI agent for every environment that isn’t production, such as staging, a preview environment or a developer’s machine. Give it its own secrets and its own allowed origins. A separate workspace for testing is even better: Keloa joins a new signed-in user to an existing contact by verified email across the whole workspace, so test users with real addresses could otherwise land on real contacts. A second secret on your production agent is not a separate environment. Keloa keys each user on the agent and the user_id, not on the secret that signed the token. Any token signed with any active secret of the production agent is the production user with that user_id, whichever environment signed it. If staging signs a token for its own user 42, that token opens production user 42’s conversations, can write in them, updates their contact and lets the AI use their verified email. Staging databases often copy production users or reuse their IDs, and more people can usually sign in there.
  • Never copy a secret of your production agent, or its key ID, to staging or development.
  • Never sign a token for a production user from staging or development.
  • Don’t add staging or development hosts to your production agent’s Allowed origins. Add them to the separate agent instead.

What your team sees

In the Inbox, a conversation started by a signed-in user shows a Verified user block in the context drawer:
  • Signed in on your site or app during this chat, with when Keloa last verified the user.
  • User ID — the user_id your site signed.
  • Every attribute your site sent. The first 4 are shown; click Show all to see the rest.
The contact’s email carries a Verified badge while it is the address your site signed as verified for that user. If a teammate changes the email, the badge disappears. On an anonymous website conversation, the email carries a Typed in chat badge only when the visitor typed that address in the chat: nobody has confirmed it’s theirs. An address the contact got another way, for example from an email or from a teammate, shows no badge there, but the AI still doesn’t use it on that conversation. Your teammates’ inbox works differently: the Shopify and WooCommerce sections in the context drawer look up the store customer and their latest orders by whatever email the contact has, including an address a visitor only typed in the chat, and they don’t label it as unverified. Don’t discuss those orders with an anonymous visitor until they’ve proven they own the address. If you use the TRONVoice integration, its panel in the inbox tells you who you’re linking on a website conversation:
  • On a signed-in user’s conversation it says Verified: the person is signed in on your site, and a link made there applies to their conversations, and on email only to mail from the address your site verified for them.
  • On an anonymous conversation it says Not verified: a link made there applies to that conversation only, and only while that one visitor is the only one writing in it. It never applies to the contact’s email or other conversations, also not after a contact merge. Before linking, Keloa asks you to confirm with Link anyway, because the AI can then share that client’s invoices, quotes and subscriptions in the conversation. The AI stops using the link once the conversation has messages from anyone else: another visitor, messages merged in from another channel or another contact, or any email reply, including one from the address owner. Keloa refuses to link from a conversation that already has such messages, and from a mixed conversation (see Website conversations merged before the rule).
  • On a website conversation from before Keloa told website visitors apart, the panel says a link can’t be made there. Keloa refuses the link, and a link made from it earlier no longer gives the AI any data. Link the client from the email conversation instead, or ask the visitor to sign in or start a new chat.
The block only appears on conversations the signed-in user started. An anonymous conversation on the same contact doesn’t show it, because it proves nothing about who is typing. A website conversation only merges with conversations of the same visitor or the same signed-in user. Keloa refuses to merge it with another visitor’s or another signed-in user’s conversation, with an older website conversation that can’t be shown to come from that visitor, or with an email, WhatsApp or social media conversation, and the merge picker shows those conversations locked with the reason. A conversation that a merge made before that rule mixed with another person’s conversation is never shown to the visitor again, and the AI and the emails your team send from it leave out what Keloa can’t attribute; the inbox shows everything. See Website conversations merged before the rule. See Contact profile.

Guests, sign-in, sign-out and devices

Signed-in conversations are never cached in the browser, and a token is never part of a page, so the next person on a shared computer can’t see them or sign the widget in again with Back.

Troubleshooting

When Keloa rejects a token, the widget request returns 401 with {"error": "identity_invalid", "reason": "<code>"}. Find it in your browser’s Network tab on the request to /widget/v1/identify or /widget/v1/messages.

FAQ

No. The token must be signed with your secret, and the secret must never reach the browser. If your site has no server-side sign-in, keep identity verification Off.
Keloa no longer accepts unsigned identity: anyone could claim any email address that way. An identify call without a token or getToken is ignored with a console warning. Move the profile fields into a signed token.
No. Keloa accepts HS256 only.
No. Secrets belong to one agent. If several agents run on your sites, create a secret for each and sign with the matching secret and agent ID.
No. A guest conversation stays anonymous. After sign-in, the widget shows the user’s own signed-in conversations.
Yes. Until it expires, anyone holding it can chat as that user, and a live-update connection they opened with it keeps receiving that conversation’s new replies for up to 13 minutes after its exp. Revoking the secret cuts both off at once. Keep it short-lived, out of your pages’ HTML, URLs, storage and logs, and never let a cache keep it.
Create a separate AI agent for development, with its own secret, and add localhost or your *.test host under that agent’s Allowed origins. Point your local .env at that agent’s ID, key ID and secret. Don’t add development hosts to your production agent, and never use a production secret on your machine: any token it signs is the production user with that user_id. Over http, identity verification only works for localhost, 127.0.0.1, *.test and *.localhost. See Staging and development.
Deleting a contact also deletes their verified identities and synced profile, and the companies Keloa created from that sync that no other contact uses. See Data & privacy.

Web widget

Install the widget and configure allowed origins.

Contact profile

What verified users look like in Keloa.

Tools

How AI tools use verified emails.

Members & roles

Who can manage identity verification.