Identity verification
Recognise signed-in users on every device and stop anyone from chatting as someone else, by signing each user ID on your server with a secret.
On this page
When your website has signed-in users, identity verification lets Pengo recognise them for sure. A verified user sees the same conversations on their laptop, phone and work computer, and your team sees one contact instead of several anonymous visitors. Because your server signs each user ID with a secret, nobody can pretend to be another user by calling pengo('identify') with someone else’s ID.
It works in three parts: a secret you keep on your server, a hash your server computes for each signed-in user, and an identify call on your pages.
#Set up identity verification
-
Create your identity secret.
In Pengo, go to Settings Security Identity verification and click Generate secret.
-
Store the secret on your server.
Click Reveal, then Copy. Save it as an environment variable called
PENGO_IDENTITY_SECRETon your server. Never put it in page code, a mobile app, or anywhere a browser can read it. -
Compute the user hash on your server.
The hash is an HMAC-SHA256 of the user’s ID, keyed with your secret, as lowercase hex. Compute it for the signed-in user and pass it to the page along with their ID:
Compute the hash on your server for the signed-in user and pass it to the page.
import crypto from "node:crypto"; // PENGO_IDENTITY_SECRET: the secret from Pengo settings. Never send it to the browser. export function pengoUserHash(userId) { return crypto .createHmac("sha256", process.env.PENGO_IDENTITY_SECRET) .update(String(userId)) .digest("hex"); }Compute the hash on your server for the signed-in user and pass it to the page.
import hashlib import hmac import os def pengo_user_hash(user_id) -> str: secret = os.environ["PENGO_IDENTITY_SECRET"].encode() return hmac.new(secret, str(user_id).encode(), hashlib.sha256).hexdigest()Compute the hash on your server for the signed-in user and pass it to the page.
<?php function pengo_user_hash($userId): string { return hash_hmac('sha256', (string) $userId, getenv('PENGO_IDENTITY_SECRET')); }Compute the hash on your server for the signed-in user and pass it to the page.
require "openssl" def pengo_user_hash(user_id) OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("PENGO_IDENTITY_SECRET"), user_id.to_s) end -
Identify the user on your pages.
On pages where the user is signed in, call
pengo('identify')after the Pengo snippet with their ID and the hash from your server:After the Pengo snippet, on pages where the user is signed in. Call pengo('shutdown') when they sign out so the next person on this computer starts fresh.
<script> pengo('identify', { userId: '{{ user.id }}', userHash: '{{ pengo_user_hash(user.id) }}', // computed on your server name: '{{ user.name }}', email: '{{ user.email }}', attributes: { plan: '{{ user.plan }}' } }); </script> <!-- On sign out: --> <script> document.querySelector('#logout').addEventListener('click', function () { pengo('shutdown'); }); </script>Identify whenever the signed-in user changes and shut down on sign-out. Get userHash from your API, never compute it in the browser.
import { useEffect } from "react"; declare global { interface Window { pengo?: (...args: unknown[]) => unknown } } export function usePengoIdentity(user: { id: string; name?: string; email?: string; pengoHash: string } | null) { useEffect(() => { if (!user) return; window.pengo?.("identify", { userId: user.id, userHash: user.pengoHash, name: user.name, email: user.email }); }, [user?.id, user?.pengoHash]); } // In your sign-out handler: export function signOut() { window.pengo?.("shutdown"); // …then your own sign-out }Compute the hash in a server component and identify from a small client component in your layout. Pengo follows client-side navigation on its own.
// app/PengoIdentify.tsx "use client"; import { useEffect } from "react"; export default function PengoIdentify({ userId, userHash, name, email }: { userId: string; userHash: string; name?: string; email?: string; }) { useEffect(() => { (window as any).pengo?.("identify", { userId, userHash, name, email }); }, [userId, userHash, name, email]); return null; } // app/layout.tsx (server component) // import crypto from "node:crypto"; // const user = await getCurrentUser(); // const userHash = user && crypto.createHmac("sha256", process.env.PENGO_IDENTITY_SECRET!).update(user.id).digest("hex"); // {user ? <PengoIdentify userId={user.id} userHash={userHash!} name={user.name} email={user.email} /> : null} // Sign-out button: onClick={() => { (window as any).pengo?.("shutdown"); signOut(); }}Watch the signed-in user and identify or shut down as it changes.
import { watch } from "vue"; export function usePengoIdentity(user) { watch( () => user.value, (current, previous) => { if (current) { window.pengo?.("identify", { userId: current.id, userHash: current.pengoHash, // from your API name: current.name, email: current.email, }); } else if (previous) { window.pengo?.("shutdown"); // signed out } }, { immediate: true } ); } -
Call
pengo('shutdown')when the user signs out.This makes Pengo forget the user on this browser and start a fresh anonymous visitor, so the next person on a shared computer can’t see their conversations. The examples above include it.
-
Turn on Require verification once every page sends the hash.
See require verification below.
#The identify call
pengo('identify', {
userId: 'user_123', // required
userHash: 'a3f1…', // from your server
name: 'Ada Lovelace',
email: 'ada@example.com',
attributes: { plan: 'pro' }
}); | Field | Required | Details |
|---|---|---|
userId | Yes | Your user’s stable ID, as text or a number, up to 255 characters. |
userHash | For verification | HMAC-SHA256 of userId with your identity secret, as 64 lowercase hex characters. |
name | No | Shown as the contact’s name, up to 120 characters. |
email | No | The contact’s email address. Ignored if it isn’t a valid address. |
attributes | No | Extra details as key and value pairs, up to 50. Values are saved as text, up to 500 characters each. |
The hash must be computed from exactly the same userId you pass to identify. If you pass a number, the hash is computed from that number written as text, as the server examples above do.
You can call identify before the widget has finished loading, and on every page load. Pengo queues it until it’s ready and only sends it again when the user or their details change. In single-page apps, call it again whenever the signed-in user changes.
#What happens with and without a valid hash
With a valid hash, Pengo links the visitor to your user:
- If the user already chatted on another device, this browser switches to that same visitor, with all their conversations.
- Conversations the visitor started on this browser before signing in move to the user.
- The first time a browser links to a user, any other sessions of that anonymous visitor are signed out, so a shared or forwarded chat link can’t follow the user into their conversations.
- The name, email and attributes you pass are saved as verified. Calls to
pengo('setVisitor')from the browser can’t overwrite them. - Your team sees a Verified badge on the contact in the inbox.
Without a valid hash, because there’s no hash, the hash is wrong or you haven’t generated a secret yet, Pengo only saves the name, email and attributes on this browser’s visitor. It never links the visitor to the user or to other devices. If the visitor is already linked to a verified user, an unverified call changes nothing.
#Require verification
Under Enforcement, the Require verification switch makes Pengo reject every pengo('identify') call without a valid userHash. Turn it on once all your pages send the hash, so nobody can attach a name or email to a visitor without your server’s signature. You need to generate a secret before you can turn it on.
#Rotate the secret
If your secret may have leaked, click Rotate secret in Settings Security Identity verification . Hashes made with the old secret stop verifying right away, so update PENGO_IDENTITY_SECRET on your server at the same time. Until you do, identify calls are treated as unverified, or rejected if Require verification is on.
#Related API calls
These calls are often used together with identify:
| Call | What it does |
|---|---|
pengo('identify', { userId, userHash, name?, email?, attributes? }) | Recognise a signed-in user on every device. Without a valid userHash only name, email and attributes are saved, never the link. |
pengo('update', { plan: 'pro' }) | Update visitor attributes (same as setVisitor). |
pengo('shutdown') | On sign-out: forget the user and their conversation on this browser and start a fresh anonymous visitor. |
pengo('newConversation', 'Prefilled message') | Open the chat on a new conversation, optionally with a prefilled message. |
pengo('track', 'cart', { value: 129 }) | Report the cart value for proactive triggers that target it. |
The JavaScript API covers every command.
#Troubleshooting
Open your browser’s developer console on a page where you call identify. Pengo logs a warning when something is off:
- identify was rejected (check userHash, or “Require verification” in settings). The call was refused. Usually Require verification is on and the hash is missing or wrong.
- identify: userHash is not valid for this userId. The hash doesn’t match. Check that your server uses the current secret, that it signs exactly the
userIdyou pass to the page, and that the hash is in hex, not Base64. - identify needs a userId. The
userIdis missing or empty.
If the user still shows as several contacts, make sure every page and device sends the same userId for them.
Common questions
Can I compute the userHash in the browser?
No. Computing it in the browser would mean putting your identity secret in page code, where anyone can read it and sign any user ID. Always compute it on your server.
What happens to chats a visitor started before signing in?
When a verified identify links this browser to a signed-in user, the conversations the visitor started anonymously on this browser move to that user.
Does the user ID have to be a number?
No. Use whatever stable ID your system has for the user, as text or a number, up to 255 characters. Don't use something that changes, like an email address the user can edit.
Was this guide helpful?
Your answer helps us improve it.
Sorry about that. or contact support, and we'll fix the guide.