Your website
Identity verification
Let your server vouch for a signed-in visitor with a JWT or an HMAC hash, so your team sees a verified contact and the assistant remembers them.
Verify who is chatting
Your website knows who is signed in; the widget does not. Identity verification lets your server vouch for a visitor, so your team sees a verified contact with their attributes and your assistant remembers their earlier conversations. It rests on one secret per assistant that only your server and bidi hold.
1. Create the identity secret
In your dashboard open Settings > Developers > Identity, pick the assistant and create its secret. It is shown once: keep it with your server's secrets, never in your page. Rotating it gives you a new one, and the previous secret keeps verifying for 24 hours, so you can deploy without signing anyone out. If the old secret leaked, "End the old secret now" stops it at once.
2. Sign a token on your server
A JWT signed with HS256 and the secret. sub is your own id for the user and is required, and so is exp, at most 24 hours ahead. email, phone, name and attributes (a flat object of up to 20 keys, 200 characters each) are optional; they sit inside the signature, so bidi trusts them.
// On your server. The secret never goes to the browser.
const { createHmac } = require("node:crypto");
function bidiIdentityToken(user) {
const b64 = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
const now = Math.floor(Date.now() / 1000);
const head = b64({ alg: "HS256", typ: "JWT" });
const body = b64({
sub: String(user.id), // required: your id for this user
email: user.email,
name: user.name,
attributes: { plan: user.plan },
iat: now,
exp: now + 60 * 60, // required: at most 24 hours ahead
});
const signature = createHmac("sha256", process.env.BIDI_IDENTITY_SECRET)
.update(head + "." + body).digest("base64url");
return head + "." + body + "." + signature;
}# On your server, with PyJWT. The secret never goes to the browser.
import os, time
import jwt
def bidi_identity_token(user) -> str:
now = int(time.time())
claims = {
"sub": str(user.id), # required: your id for this user
"email": user.email,
"name": user.name,
"attributes": {"plan": user.plan},
"iat": now,
"exp": now + 60 * 60, # required: at most 24 hours ahead
}
return jwt.encode(claims, os.environ["BIDI_IDENTITY_SECRET"], algorithm="HS256")3. Or send the user's id and its hash
If your stack cannot mint JWTs, compute HMAC-SHA256(secret, user_id) in hex on your server and pass it as user_hash. Only the id is proven this way: an email and a name sent beside it are shown to your team and are never used to match the visitor to a contact that already exists.
const { createHmac } = require("node:crypto");
const userHash = createHmac("sha256", process.env.BIDI_IDENTITY_SECRET)
.update(String(user.id)).digest("hex");import hashlib, hmac, os
user_hash = hmac.new(os.environ["BIDI_IDENTITY_SECRET"].encode(), str(user.id).encode(), hashlib.sha256).hexdigest()4. Tell the widget
Hand the token (or the id and its hash) to identify once your user is signed in, and call identify(null) when they sign out. The widget sends it to bidi, which checks it and links the conversation to the contact. Signing out, or another user signing in on the same device, starts a fresh conversation, so nobody reads someone else's chat.
bidi('identify', { token: tokenFromYourServer });
// or: bidi('identify', { user_id: 'u_42', user_hash: userHash, name: 'Sara' });
bidi('identify', null); // they signed outWhat bidi checks
The algorithm is pinned to HS256: a token that names none, another algorithm or another spelling is refused before any key is used. Signatures and hashes are compared in constant time. The exp claim is required; a token that expired more than 60 seconds ago, or that is valid for more than 24 hours, is refused. A claim that does not verify answers 401 and nothing of it is stored; repeated refusals from one address are slowed down. While the assistant has no secret, whatever the page says is kept as an unverified claim that nothing trusts.
Memory across visits
When a verified contact's conversation ends, bidi keeps a short summary of it for that assistant. Their next conversation begins with the assistant reading it, marked as notes from earlier conversations and never as instructions. Visitors nobody verified get no memory. Your team reads it on the contact and can clear it at any time; deleting the contact deletes it.
From your own code
Server to server, send the same object as contact on a chat call, and manage the people themselves under /api/v2/contacts with a key that carries contacts:read or contacts:write. A conversation proven to be one person's is never continued as another: send a different verified contact with its conversation_id and the call answers 409 identity_mismatch, so start that person's own conversation. A key limited to some agents reaches the contacts of those agents, and cannot create or delete contacts.
curl https://www.bidii.ai/api/v2/agents/AGENT_ID/chat \
-H "Authorization: Bearer bidi_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "Is my order on its way?"}],
"contact": {"token": "HEADER.PAYLOAD.SIGNATURE"}}'
curl https://www.bidii.ai/api/v2/contacts \
-H "Authorization: Bearer bidi_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"external_id": "u_42", "name": "Sara", "email": "sara@example.ae", "attributes": {"plan": "Gold"}}'