Identity and data
An agent answering a question is far better at it when they can see who is asking, what they pay you, and what they were doing thirty seconds ago. All of that is pushed from your page, and none of it is guessed.
Naming a visitor#
Three commands, safe to push on every page load. Push them as early as you like — the queue holds anything sent before the widget has booted.
// Everything your agents need to know about who this is.
$connect.push(["set", "user:nickname", ["Jane Okafor"]]);
$connect.push(["set", "user:email", ["jane@acme.com", window.CONNECT_EMAIL_HMAC]]);
$connect.push(["set", "user:avatar", ["https://acme.com/avatars/jane.jpg"]]);- user:nicknamestring, ≤ 120 chars
- What your agents see in the inbox instead of “Visitor”. A real name, or an account name if that is more useful to the person answering.
- user:emailstring · valid address
- Also removes the widget’s offline email form — a signed-in customer should never be asked for an address you already have. Shows as unverified unless you send the second argument.
- user:avatarabsolute URL
- Must be a full
https://URL. A relative path is rejected and the visitor keeps their generated avatar.
Verifying the email#
Anyone can open a console and claim to be your best customer. The second argument to user:email is a signature that proves the address came from your backend: an HMAC-SHA256 of the email, keyed with your website’s identity secret, in lowercase hex.
// Node — on YOUR server, never in the browser.
import { createHmac } from 'node:crypto';
const emailHmac = createHmac("sha256", process.env.CONNECT_IDENTITY_SECRET)
.update(user.email) // the exact string you will send from the page
.digest("hex");
// …render it into the page alongside the embed snippet.
// <script>window.CONNECT_EMAIL_HMAC = "{{ emailHmac }}";</script>Connect recomputes it and compares in constant time. A match marks the visitor verified and your agents see a badge. A mismatch, or no signature at all, still records the address — it is usually genuine — but leaves it unverified so nobody acts on it as proof.
- Sign the exact string you will send. A trailing space or different casing produces a different signature.
- The secret is per website. It never goes near the browser, and it is not the same thing as your public website key.
- The signature is per email, not per session, so you can compute it once and cache it with the user record.
Custom data#
Key–value pairs that appear as chips in the agent sidebar and are searchable from the inbox. This is where the context that makes an answer fast belongs.
$connect.push(["set", "session:data", [[
["plan", "pro"],
["mrr", 99],
["seats", 12],
["trial_ends", "2026-09-14"],
["account", "Acme Corp"],
["admin_url", "https://acme.com/admin/orgs/8123"],
]]]);- Shapearray of [key, value] pairs
- Note the nesting: the command’s arguments are a list, and the first argument is the array of pairs. Hence the triple bracket.
- Keys≤ 50 keys · ≤ 60 chars each
- Anything beyond the cap is rejected — the whole call, not the tail.
- Valuesstring ≤ 500 chars, number, boolean, or null
- Scalars only. Objects and arrays are rejected — flatten them, or put a link to your own admin page in a value and let the agent click through.
- Merging
- Sending a key again overwrites it. Sending a shorter list does not delete the keys you left out.
Segments#
Flat labels for grouping visitors — trial, churn-risk, enterprise. Up to 20, each up to 60 characters.
$connect.push(["set", "session:segments", [["customer", "pro", "priority"]]]);Unlike custom data, a segments call replaces the visitor’s segments rather than merging. Send the complete set every time.
Timeline events#
Moments, appended to the visitor’s timeline in the agent sidebar. This is the “what were they just doing” answer.
$connect.push(["set", "session:event", [[
["checkout_started", { cart: 129.9, currency: "USD" }, "orange"],
]]]);
// Several at once — one request each, in order.
$connect.push(["set", "session:event", [[
["viewed_pricing"],
["clicked_upgrade", { from: "starter" }, "green"],
]]]);- Namestring, 1–120 chars · required
- Pick one convention and keep it. Lowercase with underscores travels well.
- Dataobject of scalars · optional
- Same value rules as custom data: strings up to 500 characters, numbers, booleans, null.
- Colourstring, ≤ 24 chars · optional
- A tint for the timeline entry, so an agent can scan for the ones that matter.
Events are one-shot. Unlike identity and custom data they are not replayed if the chat window reloads, which is what stops one checkout being recorded twice.
Collected automatically#
You do not have to send any of this, and it updates live in the agent sidebar as the visitor moves around.
- Current pageURL and title
- Re-reported on every client-side navigation, so an agent always sees where the visitor is right now — not where they landed.
- Referrer
- Where they arrived from, when the browser provides it.
- Browser, OS, device
- Derived on the server from the user-agent string.
- Languages and timezone
- From the browser, so an agent knows whether it is 3am where they are.
- Screen size
- Useful the moment someone says “the button is missing”.
- Approximate locationcountry, region, city
- From the IP address. Coarse by design — city level, never a device GPS position, and the visitor is never prompted for permission.
One person, many devices#
Connect recognises a returning visitor in two ways, and they are not equivalent.
Anonymous#
An id kept in browser storage on the Connect origin. It survives reloads and navigations, and is scoped to that browser on that device. A different browser, a private window, or cleared site data is a different visitor.
With a token id#
Set window.CONNECT_TOKEN_ID for a signed-in user and Connect maps that token to one visitor record. Their laptop and their phone become the same person, with one history, and the once-ever guarantees — like the first-visit nudge — hold across both.
Identity is never merged on email alone: an email set from a page is a claim, not proof. The token is what links.