Identity verification for the widget
Why identity verification matters
Without verification, the widget trusts whatever the browser tells it. A visitor could open DevTools and call AskVault.setUser({user_id: "[email protected]"}) to claim to be the CEO, and any identity-gated skill would treat them as that person.
With verification, your backend signs the visitor's user_id with a secret only your server knows. AskVault checks the signature. The browser can't fake it.
Two flows depend on identity verification:
- Identity-gated skills.
subscription_managerwon't reveal billing details to unverified visitors. - Plan-aware guidance rules. A guidance rule can be scoped to a visitor plan or a custom attribute, so the bot is instructed differently for an Enterprise visitor than for a Free one.
What identity verification does NOT do: change which knowledge is retrieved. A source's Audience has exactly two values, public (the default) and internal. Every public channel, widget, hosted page, public API, retrieves public sources only, for verified and anonymous visitors alike. There are no role, team, plan or per-person audience values, and verifying a visitor does not unlock internal sources for them.
How HMAC verification works
Step-by-step:
- You and AskVault share a secret string (32 to 64 characters, generated once).
- For each logged-in visitor, your backend computes
hash = HMAC_SHA256(secret, user_id). - Your frontend passes
user_id, the hash, and optional metadata to the widget. - AskVault recomputes the hash server-side using the same secret.
- If hashes match, the visitor is verified. If not, the request is rejected with HTTP 403.
The browser never sees the secret. An attacker who tampers with user_id would need to forge a matching hash, which requires the secret.
Setup walkthrough
About 15 minutes for a typical backend.
Step 1: generate the secret
- Open the widget's install settings in the Deploy area and find Identity verification.
- Generate the secret. Do this only when your backend is ready to sign (see the warning below).
- Copy the secret straight away. The full secret is shown once; afterwards only its first few characters are displayed.
- Store it in your backend's secret manager (environment variable, AWS Secrets Manager, Vault and so on).
Warning: generating the secret turns enforcement on immediately. From that moment, a widget that calls setUser without a valid hash is refused. Build and deploy the signing in steps 2 and 3 first, using a test workspace if you need to try it out.
The secret is a random string starting with ivs_. Never check it into git, log it, or expose it client-side.
Step 2: compute the hash on your backend
Per logged-in visitor:
Node.js example.
const crypto = require('crypto');const secret = process.env.ASKVAULT_IDENTITY_SECRET;const userId = currentUser.id; // e.g., "user_12345"const hash = crypto.createHmac('sha256', secret).update(userId).digest('hex');Python example.
import hmac, hashlib, ossecret = os.environ["ASKVAULT_IDENTITY_SECRET"]user_id = current_user.id # e.g., "user_12345"hash_value = hmac.new( secret.encode("utf-8"), user_id.encode("utf-8"), hashlib.sha256).hexdigest()Ruby example.
require 'openssl'secret = ENV['ASKVAULT_IDENTITY_SECRET']user_id = current_user.idhash = OpenSSL::HMAC.hexdigest('SHA256', secret, user_id)Pass user_id and hash to your frontend (e.g., as data attributes on a hidden DOM element or a global JS variable).
Step 3: identify the visitor in the widget
In your frontend, after the widget script loads:
window.AskVault.setUser({ user_id: "user_12345", hash: "abc123...64hexchars", name: "Alice Chen", plan: "enterprise"});The name, email, plan, and any additional attributes are optional. Only user_id and hash are required.
The install snippet loads the widget with async, so an inline script placed right after it can run before window.AskVault exists and fail with "AskVault is not defined". If your code may run that early, define the call queue first and use setUser. The widget runs the queued call as soon as it loads:
<script> window.AskVault = window.AskVault || { _pending: [], setUser: function (d) { this._pending.push({ type: 'setUser', data: d }); } }; AskVault.setUser({ user_id: "user_12345", hash: "abc123...64hexchars" });</script>Step 4: check enforcement
Enforcement is already on once you have generated a secret. The Enforce identity verification switch in the same settings lets you turn it off temporarily while you fix your backend, and back on again.
With enforcement on, widget requests without a valid hash get HTTP 403. Anonymous visitors can still chat; what they lose is identity-gated skills and plan-aware guidance rules, not access to knowledge: retrieval is the same public corpus either way.
Signed-in visitors' chat history (rolling out)
A signed-in visitor's conversations are keyed to their user_id, so the widget can restore them on another device. With enforcement on, AskVault is rolling out a check that every request for that history needs the visitor's hash: listing or reopening past conversations, continuing the open one, reaching a human, and in-widget sequence messages. A request that names the visitor's user_id without a valid hash for it is refused with HTTP 403, even when it leaves out the user details entirely. The widget sends the hash for you once setUser() (or the data-user-hash attribute) has it. This check is not on for any workspace yet.
With enforcement off, AskVault does not check the hash, so knowing a visitor's user_id may be enough to restore their history. If your user_id values are guessable (an email or a sequential number), turn on enforcement before you rely on cross-device history.
What the bot does with verified identity
Three changes when a visitor is verified:
- Plan-aware guidance. Guidance rules scoped to a plan or custom attribute start matching, so the bot follows different instructions for an Enterprise visitor than for a Free one. This changes how the bot answers, not which sources it reads.
- Skill access. Identity-gated skills (subscription_manager, refund_processor) only fire for verified visitors.
- Audit trail. Every conversation logs
identity_verified: trueplus the verifieduser_idfor compliance.
Retrieval is not one of them. Verified and unverified visitors on a public channel read the same public-audience corpus; internal sources are excluded for both.
Field reference
The setUser() method accepts:
| Field | Required | Description |
|---|---|---|
user_id | Yes | Your unique ID for the visitor. Used for the HMAC. |
hash | Yes | HMAC-SHA256 of user_id with the shared secret. |
name | No | Display name. Used in welcome message. |
email | No | Email address. Used by lead-capture and ticketing skills. |
plan | No | The visitor's plan (e.g., "enterprise", "growth"). Matched by plan-scoped guidance rules. Does not affect retrieval. |
attributes | No | Free-form JSON for additional context (company size, role, etc.). |
Server-side verification details
Per the actual implementation:
expected = HMAC_SHA256(secret, user_id)verified = constant_time_compare(expected, hash)A constant-time comparison prevents timing attacks. The HMAC uses SHA-256 (not SHA-1 or MD5).
If user_id is missing, the visitor is treated as anonymous. If hash is missing but enforcement is on, the request fails with 403.
Audit and compliance
Every conversation records whether the visitor's identity was verified (true or false), along
with the user id you passed. A verified badge shows on the customer in the Inbox details panel.
The verified flag is not included in the Inbox CSV export, and there is no public API that returns it today.
Rotating the secret
Best practice: rotate the secret every 6 to 12 months, or immediately if compromise is suspected.
There is no grace period. Generating a new secret replaces the old one immediately, and hashes made with the old secret stop working at once.
To rotate with as little disruption as possible:
- Prepare your backend to switch secrets with a config change, so the swap takes seconds.
- Pick a quiet time.
- Generate the new secret in the widget's Identity verification settings and copy it.
- Deploy it to your backend straight away. Signed-in visitors whose pages still carry an old hash may be refused until they reload.
If you need a longer window, turn enforcement off just before rotating, deploy the new secret, then turn it back on.
Common pitfalls
Hash mismatch on every request. Usually one of: wrong secret on backend, hashing the email instead of user_id, encoding mismatch (UTF-8 vs ASCII), or whitespace in the user_id.
Expecting verified visitors to see extra content. They won't, and no setting changes that. Audience is not a per-visitor permission: setting a source to internal hides it from every visitor on a public channel, verified or not. If some knowledge must reach only a subset of people, keep it out of the public workspace rather than tagging it internal and expecting verification to unlock it.
HTTP 403 after enabling enforcement. Some pages on your site don't pass the identity. Either pass identity on every page or limit enforcement to authenticated routes (not the public widget).
Browser console shows the secret. Never expose the secret in client-side code. Compute the hash server-side only.
Hash exposed in URL. Don't put the hash in a query string; it ends up in browser history and server logs. Pass it via JavaScript only.
Planned enhancements (on the roadmap)
Features documented for accuracy but not yet shipped:
- Per-session token rotation. Today the hash is stable per user_id; planned to support short-lived per-session tokens.
- JWT-based identity. As an alternative to HMAC, accept signed JWTs from your IdP. Useful for teams already issuing JWTs.
- Native SAML/OIDC bridge. Forward an IdP assertion directly without a backend hashing step. Targeting Enterprise.
Each ships when prioritized. The HMAC flow described above is production-ready today.
FAQ
Do I need identity verification for an anonymous-visitor widget?
No. Identity verification is for logged-in visitors. Anonymous-only deployments skip it.
Can I use email as the user_id?
Yes if the email is a stable identifier. Best practice is to use your internal user ID (immutable) rather than email (changeable).
How long is the hash valid?
Indefinitely. The hash is a function of user_id and the secret, both of which don't change per request. Rotate the secret periodically.
What if my user_id changes for a visitor?
Compute a fresh hash. The old hash for the previous user_id won't verify the new one.
Is this secure against replay attacks?
The hash itself isn't replay-protected (same user_id always hashes the same). For higher-stakes flows, combine with session timestamps or use the planned JWT-based identity flow.