# Identifying your users

Reports are never anonymous. There are two ways a report is tied to a person:

- **Signed**: your server vouches for the visitor by signing their identity with your site's secret key. Their report is accepted immediately, and the email and name fields are hidden in the widget.
- **Unsigned**: the visitor types their email, and their report waits until they confirm that address by clicking a link in an email.

## How signing works

1. Your server computes `user_hash`: the lowercase hex **HMAC-SHA256** of the user's `external_id`, using the site's **Secret key** (`ur_sec_…`) as the key.
2. Your page sets the identity **before** the widget script loads:

```html
<script>
  window.userreact = {
    identity: {
      external_id: "42",
      email: "ada@example.com",
      name: "Ada Lovelace",
      user_hash: "…"
    }
  };
</script>
<script type="module" data-userreact data-key="ur_pub_…" src="…/widget/v1/ur.js"></script>
```

3. Userreact recomputes the hash. If it matches, the reporter is marked verified for that site from then on.

Fields:

- `external_id`: your own id for the user. It must be a JSON string: write `"42"`, not `42`; a number is rejected.
- `email`, `name`: shown on the report.
- `user_hash`: the signature. Compute it on your server only. Never put the secret key in browser code.

Find your keys on the site's page, in the **Keys** card. The site page's **Identify your users** card has ready-made examples for **Laravel** and **Node.js** with a **Copy example** button, and the full Laravel guide (including Inertia and Livewire) is at Docs → Identify your users → Laravel.

### Example: Node.js

```js
import crypto from 'node:crypto';

const userHash = crypto
  .createHmac('sha256', process.env.USERREACT_SECRET)
  .update(String(user.id))
  .digest('hex');
```

### Example: Laravel (Blade partial)

```blade
{{-- resources/views/partials/userreact.blade.php, included before the widget script --}}
@auth
    <script>
        window.userreact = {
            identity: @js([
                'external_id' => (string) auth()->id(),
                'email' => auth()->user()->email,
                'name' => auth()->user()->name,
                'user_hash' => hash_hmac('sha256', (string) auth()->id(), config('services.userreact.secret')),
            ]),
        };
    </script>
@endauth
```

### Checking a signature by hand

```sh
printf '%s' '42' | openssl dgst -sha256 -hmac 'ur_sec_…'
```

The result must equal the `user_hash` your page sends for `external_id` `"42"`. Common mistakes: hashing the email instead of the `external_id`, sending `external_id` as a number, using the public key instead of the secret key, or setting `window.userreact` after the widget script.

## Without signing: email confirmation

When a visitor is not signed:

1. They type their email in the widget and send the report.
2. The report is stored but hidden: it is not in your inbox and does not count against your plan yet.
3. They receive an email, "Confirm your email to send your feedback to {your domain}", explaining that the site does not accept anonymous reports.
4. When they click the link, their waiting reports are released to your inbox and the page says "Your report is on its way". A visitor who confirmed earlier sees "Already confirmed".

The link is valid for 7 days. If it has expired, the page offers **Email me a new link**. Confirmation emails are sent at most every 15 minutes to the same person.

If your account is at its plan's report limit when the visitor confirms, their reports wait — nothing is lost — and are released when there is room.