Track
Identify users
Link a visitor to your own user id after signup and login, so later payments are credited to the channel that brought the person.
identify connects the anonymous visitor to a user id from your own database. Without it, ProductLense can count visitors but cannot tell which channel a paying customer came from.
window.productlense('identify', 'user_123', { email: '[email protected]', plan: 'starter' });When to call it
- After signup, once the account exists.
- After every login.
- On page loads for a signed-in user, if that is simpler. Repeated calls with the same id are harmless.
Each call sends a $identify event and attaches the id as uid to every later event in the same tab. The id is kept in sessionStorage, so it survives reloads but not a new tab; call identify again when a signed-in user opens a new tab.
Which id to use
Use the stable id of the user in your database: a UUID, a numeric id passed as a string, or an auth provider id such as auth0|abc. The id may be any non-empty string of at most 128 UTF-8 bytes without control characters, so an email address also works. Calls with any other value are ignored.
The second argument is optional traits, a JSON object stored as props on the $identify event. Send the user's email there: ProductLense stores it, lower-cased, with the identity, so a payment whose billing customer has the same email is matched to this visitor even when the customer carries no user id.
Show names and avatars
Send name and an image (or avatar) URL in traits and ProductLense shows the person instead of an anonymous visitor: on the live globe and its activity feed, in Visitors, Customers and the Events page, and on each visitor's page.
window.productlense('identify', 'user_123', {
email: '[email protected]',
name: 'Ada Lovelace',
image: 'https://lh3.googleusercontent.com/a/abc=s96-c',
});- Name. Up to 80 characters; markup characters are removed. A later call without a name keeps the stored one, a new name replaces it.
- Image. Only
httpsURLs on these avatar hosts are kept:gravatar.com(andsecure./www.),lh3.googleusercontent.com,avatars.githubusercontent.com,pbs.twimg.com,cdn.discordapp.com,avatar.vercel.sh,img.clerk.com,images.clerk.devand*.supabase.co. Anything else is dropped and the person gets initials. This keeps the dashboard from loading images from arbitrary hosts. - Anonymous visitors get a stable two-word name for their session, such as "Cyan Chipmunk", so the live feed is readable without identifying anyone.
- Public dashboards show identified names masked ("Ada** L**"), never the avatar or the visitor id.
Identify from your server
When the user logs in on your backend, or from a mobile app's API, call POST /api/v1/identify with an API key that has the events scope (Scale plan; Settings → API keys):
curl -X POST https://api.productlense.app/api/v1/identify \
-H "Authorization: Bearer mk_live_..." \
-H "Content-Type: application/json" \
-d '{"user_id":"user_123","visitor_id":"3f9a1c0b7d2e4a51","traits":{"name":"Ada Lovelace","email":"[email protected]","image":"https://secure.gravatar.com/avatar/..."}}'user_idis required (at most 128 characters).visitor_idis optional: the 16-character id fromwindow.productlense.visitorIdor the/api/videndpoint. With it, the call links that visitor to the user, as a browseridentifydoes, including the email for payment matching. Without it, only the user's traits are updated, for every visitor already linked to the user.traits.emailmust be an email address;nameandimagefollow the rules above.- The response is
{ "user_id": "user_123", "linked": true }. Errors: 400 invalid body, 401 missing or revoked key, 403 key without theeventsscope.
Put the same id on the billing customer
A payment is matched to a visitor through the customer in your billing provider. The most reliable link is the same user id stored on that customer:
| Provider | Where to put the user id |
|---|---|
| Stripe | Customer metadata pl_user_id |
| Paddle | Customer custom data pl_user_id |
| Razorpay | Customer or payment notes pl_user_id |
| Polar | Customer external id |
| Chargebee | Custom field cf_pl_user_id or cf_user_id |
| Payments API | customer.user_id |
ProductLense reads the metadata keys pl_user_id, user_id, userId and uid, in that order. Each provider page under Connect billing says where its metadata lives.
Framework examples
app/productlense-identify.tsx
'use client';
import { useEffect } from 'react';
export function ProductLenseIdentify({ userId }: { userId?: string }) {
useEffect(() => {
if (userId) window.productlense?.('identify', userId);
}, [userId]);
return null;
}src/useProductLenseIdentify.ts
import { useEffect } from 'react';
export function useProductLenseIdentify(user: { id: string } | null) {
useEffect(() => {
if (user) window.productlense?.('identify', user.id);
}, [user]);
}src/routes/+layout.svelte
<script lang="ts">
let { data, children } = $props();
$effect(() => {
if (data.user) window.productlense?.('identify', data.user.id);
});
</script>
{@render children()}plugins/productlense.client.ts
export default defineNuxtPlugin(() => {
const user = useState<{ id: string } | null>('user');
watch(
user,
(u) => {
if (u) window.productlense?.('identify', u.id);
},
{ immediate: true },
);
});functions.php
<?php
add_action('wp_footer', function () {
if (!is_user_logged_in()) return;
printf(
'<script>window.productlense=window.productlense||function(){(window.productlense.q=window.productlense.q||[]).push(arguments)};window.productlense("identify",%s);</script>',
wp_json_encode((string) get_current_user_id())
);
});For TypeScript, declare window.productlense once as shown in Custom events.
How it affects attribution
A visitor who identifies joins every visitor that used the same id. When that user pays, the earliest first touch among those visitors wins. Payments made before the identify call arrives are retried every night for 90 days. See How payments are attributed.
Privacy
identify links pseudonymous visits to a person in your system, so the data linked to that id is personal data you control. Do not send special category data (health, religion and so on) as traits. See GDPR and India DPDP.