Identity verification

Tell your agent which of your users it's talking to, and prove it. Your server signs the user's id with a secret only you and IntelliDesk know, so nobody can chat as someone else.

What it gives you

  • The agent knows the user: it can greet them by name and use the details you send (plan, company…).
  • Their chats land on one contact in IntelliDesk, across devices, with what they told the agent before.
  • Actions such as order lookups use the verified user, not whatever someone types.
  • Without a signature, a user id is only a claim: anyone could open the widget and say they're someone else. With one, IntelliDesk only trusts ids your server signed.

Set it up

  1. In the dashboard, open Deploy → Chat widget → Embed and turn on Identity verification. Copy the identity secret into your server's environment (for example INTELLIDESK_IDENTITY_SECRET). Never put it in browser code.
  2. For each signed-in user, compute user_hash: the HMAC-SHA256 of their user_id with that secret, as lowercase hex. Use the secret as text, exactly as shown; don't hex-decode it.
  3. Give both to the widget, either before the script loads or later with identify:
HTML
<script>
  window.intellideskUserConfig = {
    user_id: "u_123",                 // your user's id
    user_hash: "HASH_FROM_YOUR_SERVER", // HMAC-SHA256 of user_id, below
    name: "Ada Lovelace",             // optional: shown to the agent
    email: "ada@example.com",
    plan: "pro",
  };
</script>
<script src="https://YOUR_APP/widget.js" data-agent="AGENT_SLUG_OR_ID" async></script>

In a single-page app, fetch the signed values from your server once the user signs in and call intellidesk("identify", { user_id, user_hash, name, email }). On logout, call intellidesk("resetUser"): the next person on that browser starts as a new visitor. See React and Next.js.

Sign on your server

Each of these returns the same hex string for the same secret and user id.

Node.js
import { createHmac } from "node:crypto";

// INTELLIDESK_IDENTITY_SECRET: Deploy → Chat widget → Embed → Identity secret. Server only.
export function intellideskHash(userId: string) {
  return createHmac("sha256", process.env.INTELLIDESK_IDENTITY_SECRET!).update(userId).digest("hex");
}

// Express: the signed-in user's identity, for the browser to pass to identify().
app.get("/api/intellidesk-identity", requireLogin, (req, res) => {
  res.json({ user_id: req.user.id, user_hash: intellideskHash(req.user.id) });
});
Python
import hashlib, hmac, os

def intellidesk_hash(user_id: str) -> str:
    secret = os.environ["INTELLIDESK_IDENTITY_SECRET"].encode()
    return hmac.new(secret, user_id.encode(), hashlib.sha256).hexdigest()
PHP
<?php
function intellidesk_hash(string $userId): string {
    return hash_hmac('sha256', $userId, getenv('INTELLIDESK_IDENTITY_SECRET'));
}
Ruby
require "openssl"

def intellidesk_hash(user_id)
  OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("INTELLIDESK_IDENTITY_SECRET"), user_id.to_s)
end
Go
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"os"
)

func IntelliDeskHash(userID string) string {
	mac := hmac.New(sha256.New, []byte(os.Getenv("INTELLIDESK_IDENTITY_SECRET")))
	mac.Write([]byte(userID))
	return hex.EncodeToString(mac.Sum(nil))
}

User ids

Up to 256 visible characters, no spaces: database ids, emails (ada@example.com) and sign-in provider ids (auth0|64f1…, google-oauth2|1234) all work. Sign exactly the string you send. Prefer an id that never changes: a user who changes their email would otherwise become a new contact.

Or send a JWT

A JWT carries the user's details inside the signature, so the agent can trust their email and name too, and it can expire. Sign it with HS256 and the same identity secret.

Node.js
import jwt from "jsonwebtoken";

// A short-lived token with the details you want the agent to trust.
const token = jwt.sign(
  {
    user_id: user.id,                 // derive from the authenticated session
    aud: process.env.INTELLIDESK_AGENT_ID,
    workspace_id: process.env.INTELLIDESK_WORKSPACE_ID,
    customer_id: user.accountId,       // optional account mapping owned by your server
    email: user.email,
    name: user.name,
    phone: user.phone,
    custom_attributes: { plan: "pro", company: "Acme" },
  },
  process.env.INTELLIDESK_IDENTITY_SECRET,
  { algorithm: "HS256", expiresIn: "1h" },
);
Browser
window.intellidesk("identify", { token: TOKEN_FROM_YOUR_SERVER });
ClaimWhat it is
user_id or subRequired. The user's id (same rules as above).
email, name, phoneSaved on the contact and trusted by the agent.
custom_attributesAn object of extra details (plan, company…), shown to your team and the agent.
exp, nbf, iatNumeric dates in seconds. Expired, not-yet-valid, or malformed dates are refused. Set exp: a legacy token without it never expires.
aud / agent_id, workspace_idUse the agent ID as aud and include its workspace ID. Supplied scopes must match; keep each agent identity secret separate.
customer_idOptional account ID, selected by your authenticated server session. Never copy it from browser input.
stripe_accountsOptional. The user's Stripe customer IDs (cus_…), as a list. Lets the agent answer billing questions from your Stripe account.

Details outside the signature

Other fields you pass (name, plan, current_page, or a metadata object) are shown to the agent as context, but anyone can change them in their browser, so treat them as hints. Don't put secrets or payment ids there; use JWT claims for anything the agent must trust.

While verification is on

WhereWhat happens
Chat widget, help pageWith Only signed-in users can chat on (the default), only verified users can chat; others see “Please sign in to chat with us.”, and your browser console explains why. Turn it off to let anonymous visitors chat too. Unsigned user IDs are ignored when verification is configured. An explicitly presented forged or expired proof is refused, even on an existing chat.
REST API and MCPNot affected: the secret key already proves the call is yours. Name the user with user (REST API).
Email, WhatsApp, Messenger, Instagram, SlackNot affected: those channels identify people their own way.
PlaygroundNot affected.
Rotating the secret (Deploy → Chat widget → Embed → Rotate) makes every earlier hash and token invalid for new requests and room deliveries. Update your server's secret at the same time. Refresh a JWT with identify before it expires; every signed chat continuation and history request needs fresh proof.

Authorize customer actions

Anonymous visitors can still ask FAQs. Account lookups require current server-verified identity; a remembered contact, typed email, order number, or model assertion cannot grant account access. HMAC verifies the user ID only. Use JWT claims or the authenticated REST API user for trusted email and account IDs.

For Custom Action HTTP, enable Verified customer only. Use {{verified_user_id}}, {{verified_email}}, or {{verified_customer_id}} in your URL/body. These placeholders also enable the policy automatically. The AI cannot fill or replace them. Missing required signed claims block the request.

Your API receives X-IntelliDesk-Customer-Id and, when signed, X-IntelliDesk-Customer-Email. Configure a server credential in the action headers and authenticate it at your API before trusting those headers. Your API must check every order/resource against that customer before reading or changing it. Return a JSON ownership envelope:

Customer-scoped response
{
  "customer": { "user_id": "u_123" },
  "data": { "status": "shipped" }
}

The returned customer.user_id must match the signed subject. A mismatched or missing owner suppresses customer data and reports unconfirmed completion. Check the provider's state before retrying writes; IntelliDesk cannot undo a request your API already accepted.

Shopify order lookup compares the returned order email with the email signed in your JWT or authenticated API user. It ignores emails typed into chat. The generic Procedures order endpoint uses the same headers and requires customer.user_id beside its existing order object. Update existing integrations before using them for account data.

Existing custom actions without this policy remain general-purpose integrations. Enable it for account-specific actions and make your API enforce resource ownership.

Stripe billing answers

Turn on Stripe (customer billing) in Actions and your agent can tell a signed-in customer about their plan, invoices and card on file, straight from your Stripe account. It only reads; it can't cancel, refund or change a plan.

  • Sign the customer's Stripe IDs in your JWT as stripe_accounts, for example [{ "stripe_id": "cus_123" }]. Or sign their email: it works when it matches exactly one Stripe customer.
  • An HMAC user_hash only proves the user ID, so it isn't enough for billing on its own.
  • An email or customer ID typed into the chat never unlocks billing details.

Check the signed customer on your MCP server

When an MCP server under Actions → MCP server has Only for verified customers on, each tool call tells your server which signed-in customer is asking. The note comes in the X-IntelliDesk-Customer header and again in params._meta["io.intellidesk/customer"]. It holds the customer's id, their email when it was signed, and a time window of 5 minutes.

  1. On the server's page, click Show key under Customer signing key and copy it into your server's environment. You see it once. Until then, tools for verified customers don't run.
  2. On every tool call, check the note with the code below and reject the call if it fails.
  3. Only read or change data that belongs to the returned id. An order lookup, for example, must match both the order number and this customer. The note says who is asking; it doesn't make any order theirs.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// INTELLIDESK_MCP_CUSTOMER_KEY: Actions → MCP server → Customer signing key. Server only.
function verifyCustomer(token, meta, secret, now = Math.floor(Date.now() / 1000)) {
  try {
    if (typeof token !== "string" || token.length > 4096) return null;
    const parts = token.split(".");
    if (parts.length !== 2 || parts.some(part => !/^[A-Za-z0-9_-]+$/.test(part))) return null;
    const [payload, sig] = parts;
    const expected = createHmac("sha256", secret)
      .update("io.intellidesk/customer.v1." + payload).digest();
    const actual = Buffer.from(sig, "base64url");
    if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) return null;
    const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
    if (!claims || typeof claims !== "object" || Array.isArray(claims) ||
        Object.keys(claims).some(key => !["id", "email", "iat", "exp"].includes(key)) ||
        typeof claims.id !== "string" || !claims.id ||
        (claims.email !== undefined && typeof claims.email !== "string") ||
        !Number.isSafeInteger(claims.iat) || !Number.isSafeInteger(claims.exp) ||
        claims.iat > now || claims.exp <= now || claims.exp <= claims.iat ||
        claims.exp - claims.iat > 300) return null;
    if (!meta || typeof meta !== "object" || Array.isArray(meta) ||
        Object.keys(meta).length !== Object.keys(claims).length + 1 ||
        meta.id !== claims.id || meta.email !== claims.email ||
        meta.iat !== claims.iat || meta.exp !== claims.exp || meta.sig !== sig) return null;
    return claims;
  } catch { return null; }
}

// In your tools/call handler, before doing anything:
// const token = req.headers["x-intellidesk-customer"];
// const meta = body.params?._meta?.["io.intellidesk/customer"];
// const customer = verifyCustomer(token, meta, process.env.INTELLIDESK_MCP_CUSTOMER_KEY);
// if (!customer) return res.status(401).end();
// Then only read or change data that belongs to customer.id.
Use the key as text, exactly as shown; don't base64-decode it. Rotating the key makes a new one and the old one stops working, so update your server at the same time. Inputs you set to Fill from the verified customer are filled by IntelliDesk; the AI can't change them.

Test it

  1. Open your site signed in, send a message, and open the chat in Activity: it shows the user as verified.
  2. Change one character of user_hash in the browser: the widget asks the visitor to sign in, and the console says why.
  3. Sign out (your code calls resetUser) and sign in as another user: the chat starts empty.