# 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.

```js
window.productlense('identify', 'user_123', { email: 'ada@example.com', 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.

> [!NOTE]
> `$identify` events do not count toward your plan's monthly events.

## 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.

```js
window.productlense('identify', 'user_123', {
  email: 'ada@example.com',
  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 `https` URLs on these avatar hosts are kept: `gravatar.com` (and `secure.`/`www.`), `lh3.googleusercontent.com`, `avatars.githubusercontent.com`, `pbs.twimg.com`, `cdn.discordapp.com`, `avatar.vercel.sh`, `img.clerk.com`, `images.clerk.dev` and `*.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):

```sh
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":"ada@example.com","image":"https://secure.gravatar.com/avatar/..."}}'
```

- `user_id` is required (at most 128 characters).
- `visitor_id` is optional: the 16-character id from `window.productlense.visitorId` or the `/api/vid` endpoint. With it, the call links that visitor to the user, as a browser `identify` does, including the email for payment matching. Without it, only the user's traits are updated, for every visitor already linked to the user.
- `traits.email` must be an email address; `name` and `image` follow the rules above.
- The response is `{ "user_id": "user_123", "linked": true }`. Errors: 400 invalid body, 401 missing or revoked key, 403 key without the `events` scope.

## 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](/docs/revenue/connect-billing) says where its metadata lives.

> [!IMPORTANT]
> Without a user id on the customer, ProductLense falls back to the customer email, matched to the `email` trait sent with `identify` or to a user id that is that email, and then to a visitor id in the customer metadata. Send `email` in traits as well as setting the user id: the id survives an email change, the email covers customers created where you cannot set metadata.

## Framework examples

```tsx tab="Next.js" title="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;
}
```

```tsx tab="React" title="src/useProductLenseIdentify.ts"
import { useEffect } from 'react';

export function useProductLenseIdentify(user: { id: string } | null) {
  useEffect(() => {
    if (user) window.productlense?.('identify', user.id);
  }, [user]);
}
```

```svelte tab="SvelteKit" title="src/routes/+layout.svelte"
<script lang="ts">
  let { data, children } = $props();

  $effect(() => {
    if (data.user) window.productlense?.('identify', data.user.id);
  });
</script>

{@render children()}
```

```ts tab="Nuxt" title="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 },
  );
});
```

```php tab="WordPress" title="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](/docs/track/custom-events#call-it-before-the-script-loads).

## 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](/docs/revenue/attribution).

## 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](/docs/privacy/gdpr-and-dpdp).
