Skip to content
Pengo Help Center
Browse all guides

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.

Developers 5 min read Updated
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

  1. Create your identity secret.

    In Pengo, go to Settings Security Identity verification and click Generate secret.

  2. Store the secret on your server.

    Click Reveal, then Copy. Save it as an environment variable called PENGO_IDENTITY_SECRET on your server. Never put it in page code, a mobile app, or anywhere a browser can read it.

  3. 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");
    }
  4. 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>
  5. 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.

  6. Turn on Require verification once every page sends the hash.

    See require verification below.

#The identify call

JavaScript
pengo('identify', {
  userId: 'user_123',            // required
  userHash: 'a3f1…',             // from your server
  name: 'Ada Lovelace',
  email: 'ada@example.com',
  attributes: { plan: 'pro' }
});
FieldRequiredDetails
userIdYesYour user’s stable ID, as text or a number, up to 255 characters.
userHashFor verificationHMAC-SHA256 of userId with your identity secret, as 64 lowercase hex characters.
nameNoShown as the contact’s name, up to 120 characters.
emailNoThe contact’s email address. Ignored if it isn’t a valid address.
attributesNoExtra 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.

These calls are often used together with identify:

CallWhat 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 userId you pass to the page, and that the hash is in hex, not Base64.
  • identify needs a userId. The userId is 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.