Identify logged-in users
Tell the chat who is signed in so the agent greets people by name and your inbox shows whose conversation is whose. Examples for Shopify, WordPress and Next.js.
Your website knows who is signed in. The chat does not. This page shows you how to tell it.
Once you do, the agent can say "Hi Jane" instead of "Hi there", it stops asking for details it already has, and every conversation in your inbox is labelled with the person it belongs to.
The idea in three sentences
Think of a party with a doorman.
- The name tag. Your page hands the chat a note: "this is Jane, user 42, [email protected]".
- The stamp. Your server adds a stamp only it can make, using your secret key, so nobody can pretend to be Jane.
- The doorman. Stamped people get a green Verified identity badge in your inbox. You decide whether unstamped notes still count.
Start with the name tag. Add the stamp when you are ready.
Add it to your platform
Pick your platform and paste. Every snippet does the same two things: it pushes
identify with the signed-in person's details, and pushes logout when nobody
is signed in.
Before you paste
The chat widget must already be installed. For stamped, verified identities you also need a key: open Widget → Install, expand Visitor identity, and select Generate secret. Keep it on your server, never in a page.
In your theme editor open layout/theme.liquid and paste this just before
</body>:
<script>
window.FetchplyWidget = window.FetchplyWidget || { q: [] };
{% if customer %}
window.FetchplyWidget.q.push(['identify', {
userId: {{ customer.id | json }},
name: {{ customer.name | json }},
email: {{ customer.email | json }}
}]);
{% else %}
window.FetchplyWidget.q.push(['logout']);
{% endif %}
</script>Shopify fills in the logged-in customer for you, and the else branch takes
care of sign-out on every page. A theme cannot make the stamp, so leave the
identity check on Accept without proof. If you need the Verified badge,
send the stamp from your own backend instead.
First put your key in wp-config.php, above the "stop editing" line:
define('FETCHPLY_IDENTITY_SECRET', 'fpid_paste-your-key-here');Then add this to your theme's functions.php (or a small site plugin). It
stamps the identity, so conversations show as verified, and signs visitors
out when they are logged out:
add_action('wp_footer', function () {
$command = "['logout']";
if (is_user_logged_in()) {
$user = wp_get_current_user();
$email = strtolower(trim($user->user_email));
$issuedAt = time();
$identity = [
'userId' => (string) $user->ID,
'name' => $user->display_name,
'email' => $email,
'issuedAt' => $issuedAt,
'userHash' => hash_hmac(
'sha256',
"{$user->ID}\n{$email}\n{$issuedAt}",
FETCHPLY_IDENTITY_SECRET
),
];
$command = '["identify",' . wp_json_encode($identity) . ']';
}
echo '<script>window.FetchplyWidget=window.FetchplyWidget||{q:[]};'
. 'window.FetchplyWidget.q.push(' . $command . ');</script>';
});Using a full-page cache plugin? Exclude logged-in visitors from the cache, or one person's details can end up on another person's page.
Do it in a server component on your signed-in pages, so the key stays on the server:
import { createHmac } from 'node:crypto';
import Script from 'next/script';
export async function IdentifyVisitor() {
const user = await getSignedInUser(); // your own auth
const issuedAt = Math.floor(Date.now() / 1000);
const command = user
? ['identify', {
userId: String(user.id),
name: user.name,
email: user.email,
issuedAt,
userHash: createHmac('sha256', process.env.FETCHPLY_IDENTITY_SECRET!)
.update(`${user.id}\n${user.email.trim().toLowerCase()}\n${issuedAt}`)
.digest('hex'),
}]
: ['logout'];
return (
<Script id='fetchply-identity'>
{`window.FetchplyWidget = window.FetchplyWidget || { q: [] };
window.FetchplyWidget.q.push(${JSON.stringify(command).replace(/</g, '\\u003c')});`}
</Script>
);
}Render <IdentifyVisitor /> in the layout that wraps your signed-in pages.
Because it runs on the server, your key never reaches the browser.
A single-page app never reloads, so it tells the chat when the user changes. Save this hook and call it once, high in your app:
import { useEffect } from 'react';
export function useFetchplyIdentity(user) {
useEffect(() => {
window.FetchplyWidget = window.FetchplyWidget || { q: [] };
// Signed out: forget the person and clear their saved chats.
if (!user) {
window.FetchplyWidget.q.push(['logout']);
return;
}
let cancelled = false;
// Your own endpoint returns { userHash, issuedAt } for the signed-in
// user, made with the stamp recipe below. Never build it in the browser.
fetch('/api/chat-identity', { credentials: 'include' })
.then((response) => response.json())
.then(({ userHash, issuedAt }) => {
if (cancelled) return;
window.FetchplyWidget.q.push(['identify', {
userId: String(user.id),
name: user.name,
email: user.email,
userHash,
issuedAt,
}]);
})
.catch(() => {
// Chat still works, just without a name attached.
});
return () => {
cancelled = true;
};
}, [user]);
}function App() {
const user = useCurrentUser(); // your own auth, null when signed out
useFetchplyIdentity(user);
return <YourRoutes />;
}No stamp yet? Drop the fetch and push identify with just userId,
name, and email, and leave the identity check on Accept without
proof.
Anywhere you can print HTML for a signed-in visitor:
<script>
window.FetchplyWidget = window.FetchplyWidget || { q: [] };
window.FetchplyWidget.q.push(['identify', {
userId: '42',
name: 'Jane Doe',
email: '[email protected]',
userHash: 'STAMP_FROM_YOUR_SERVER', // optional but recommended
issuedAt: 1764000000 // the second you made the stamp
}]);
</script>And on pages where nobody is signed in:
<script>
window.FetchplyWidget = window.FetchplyWidget || { q: [] };
window.FetchplyWidget.q.push(['logout']);
</script>No JavaScript at all? Put the same values on the chat script tag as
data-user-id, data-user-name, data-user-email, data-user-hash, and
data-user-hash-issued-at. Only do that on pages built fresh per visitor,
never on cached pages.
Signing someone out
Sign-out matters as much as sign-in: without it, the next person on that computer can open the chat and read the previous conversation.
window.FetchplyWidget = window.FetchplyWidget || { q: [] };
window.FetchplyWidget.q.push(['logout']);- Where to put it: in your sign-out handler, and on every page where nobody
is signed in (that is what the
elsebranches in the snippets above do). Both is best. - What it does: forgets the person, clears the chats saved in that browser, and starts a fresh conversation.
- Two ways to call it:
window.FetchplyWidget.q.push(['logout'])works at any time, even before the chat has finished loading.window.FetchplyWidget.logout()does the same once it has loaded. - If you forget it: should a different person sign in on the same browser,
the chat notices the new user ID and starts a new conversation by itself. It
cannot notice a sign-out on its own, so push
logoutyourself.
Check that it worked
- Sign in on your own site and send the chat a message such as "what is my name?".
- Open Inbox and select the new conversation. You should see the person's name and user ID, plus a green Verified identity badge if you sent a stamp.
- Sign out and open the chat again. It should be empty, with no name attached.
Making the stamp
The stamp is three lines of text — user ID, email, and the current time in seconds — signed with your secret key:
<userId>\n<lower-cased email, or empty>\n<issuedAt in seconds>Sign it with HMAC-SHA256 and send the result as lowercase hex.
const { createHmac } = require('node:crypto');
const issuedAt = Math.floor(Date.now() / 1000);
const userHash = createHmac('sha256', process.env.FETCHPLY_IDENTITY_SECRET)
.update(`${user.id}\n${user.email.trim().toLowerCase()}\n${issuedAt}`)
.digest('hex');$issuedAt = time();
$payload = $user->id . "\n" . strtolower(trim($user->email)) . "\n" . $issuedAt;
$userHash = hash_hmac('sha256', $payload, getenv('FETCHPLY_IDENTITY_SECRET'));import hashlib, hmac, os, time
issued_at = int(time.time())
payload = f"{user.id}\n{user.email.strip().lower()}\n{issued_at}"
user_hash = hmac.new(
os.environ["FETCHPLY_IDENTITY_SECRET"].encode(),
payload.encode(),
hashlib.sha256,
).hexdigest()issued_at = Time.now.to_i
payload = "#{user.id}\n#{user.email.strip.downcase}\n#{issued_at}"
user_hash = OpenSSL::HMAC.hexdigest("sha256", ENV["FETCHPLY_IDENTITY_SECRET"], payload)Two easy mistakes
Trim and lower-case the email before signing it, even if you store it as
[email protected], and send the same issuedAt you signed. If the person has
no email, sign an empty line in its place.
A stamp stops working 24 hours after issuedAt, so make it when the page or
session is served, never once at signup.
What you can send
| Field | Required | Notes |
|---|---|---|
userId | Yes | Your own stable ID for this person. Without it, everything else is ignored. |
name | No | Up to 200 characters. |
email | No | Also used to match this person to existing contacts. |
phone | No | Up to 60 characters. |
avatarUrl | No | Must start with https://, otherwise initials are shown. |
attributes | No | Up to 20 extras such as { plan: 'pro' }, shown in the conversation details. Keys use lower-case letters, numbers, and underscores, starting with a letter. |
userHash | No, but recommended | The stamp from your server. |
issuedAt | With userHash | The exact second your server made the stamp. |
How strictly to check
Under Widget → Install → Visitor identity, choose what happens when a visitor arrives:
- Accept without proof (the default, and the best place to start): details are used either way, and only stamped ones get the Verified badge.
- Require proof: unstamped visitors stay anonymous. Choose this once every page sends a stamp.
- Off: sign-in details are ignored completely.
Whichever you choose, a stamp that does not match is refused rather than quietly accepted, so a mistake in your code shows up straight away instead of hiding.
Rotating your secret key
Rotate secret creates a new key and keeps the old one working for 24 hours, so you can update your server without signing anyone out. Rotate immediately if you think the key leaked.
When something looks wrong
- The agent still asks for a name or email. Details attach when the visitor
sends their first message. Check that the snippet runs on the page and that
userIdis filled in; without it, the rest is ignored. - The conversation says Unverified identity. No stamp was sent, or it did not
match. Sign exactly the three lines above with the key from this agent's
settings, and send the same
issuedAtyou signed. - Nobody is recognised at all. Check that the identity check is not set to Off, and that Require proof is not on before every page sends a stamp.
- It broke after rotating the key. Stamps made with the old key stop working 24 hours after rotation. Put the new key on your server and redeploy.
- Two people share one computer. Push
['logout']when someone signs out. It clears saved conversations on that device and starts fresh for the next person. If a different person signs in without a sign-out, the chat notices and starts a new conversation by itself. - Someone signed in halfway through a conversation. The chat starts a new conversation automatically, so two people's messages never mix.