> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usertick.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify your users

> Prove that the user ID your page sends is genuine, with a hash your server computes once per user.

`identify()` runs in the visitor's browser, so anyone can open the browser console and call it with another person's user ID. Verification lets Usertick tell a genuine ID from a typed one. Your server, which knows who is signed in, computes a hash of the user ID with a secret key that never leaves your server. Usertick computes the same hash and compares the two.

Verification is optional and available on every plan. Responses keep working without it, and an unverified identity is marked as such.

## When you need it

Verify your users when a response's identity does something on its own:

* **Contacts.** Only a verified identity creates or changes a contact in **Email → Contacts**.
* **CRMs, mailing lists and event tools.** In **Monitor** and **Enforced**, only a verified identity's email reaches HubSpot, Klaviyo, Mailchimp and the other tools that track people.
* **Workflows that act on the person**, such as a message to their email or an update to their CRM record.

When you only read in the inbox who answered, `identify()` without a hash is enough.

## How it works

```mermaid theme={null}
sequenceDiagram
    participant S as Your server
    participant P as Your page
    participant U as Usertick
    S->>S: hash = HMAC-SHA256(secret key, user ID)
    S->>P: The user ID and its hash
    P->>U: identify(user ID, traits, { hash }), sent with the response
    U->>U: Computes the same hash with your key and compares
```

The hash is the same every time for the same user ID and key, and it never expires. Your server can compute it when the user signs up, keep it with the user, and pass it on every page. A response that arrives with a matching hash is marked **Verified** in the inbox, in the API and in every integration.

## Set it up

**Settings → Developers → Identity** walks through the setup in five steps and shows whether each one is done. Members whose role has the **Manage developer settings** permission see the page.

**1. Create a secret key**

Select **Create secret key**, copy the key, which starts with `utid_`, and store it in your server's environment as `USERTICK_IDENTITY_SECRET`. Usertick shows the key only once, so copy it before you select **I saved it**. Never put the key in the page.

**2. Identify the user on your page**

Call `identify()` with the user's hash as the third argument, as in [Pass the hash to the page](#pass-the-hash-to-the-page).

**3. Compute the hash on your server**

Compute the hash with the code for your server's language in [Compute the hash](#compute-the-hash), and check it with **Test a hash**.

**4. Check the results**

Select **Turn on Monitor**. Usertick then checks every hash that arrives and lists each failure with how to fix it, while responses stay as they are.

**5. Enforce**

When the results show no more failures, select **Switch to Enforced**. Usertick first says how many identified responses so far had no valid hash.

## Compute the hash

The hash is the HMAC-SHA256 of the user ID, written as 64 lowercase hex characters, with the secret key as the HMAC key. Every language has HMAC in its standard library, so nothing needs installing.

> [!WARNING]
>
> Use the secret key exactly as shown, including the `utid_` prefix, and the user ID exactly as you pass it to `identify()`. A different spelling of either gives a different hash.

```js Node.js theme={null}
import { createHmac } from 'node:crypto';

const hash = createHmac('sha256', process.env.USERTICK_IDENTITY_SECRET) // utid_...
  .update(String(user.id))
  .digest('hex');
```

```python Python theme={null}
import hashlib, hmac, os

user_hash = hmac.new(
    os.environ["USERTICK_IDENTITY_SECRET"].encode(),  # utid_...
    str(user.id).encode(),
    hashlib.sha256,
).hexdigest()
```

```php PHP theme={null}
$hash = hash_hmac('sha256', (string) $user->id, getenv('USERTICK_IDENTITY_SECRET')); // utid_...
```

```ruby Ruby theme={null}
require 'openssl'

hash = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('USERTICK_IDENTITY_SECRET'), user.id.to_s) # utid_...
```

```go Go theme={null}
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"os"
)

mac := hmac.New(sha256.New, []byte(os.Getenv("USERTICK_IDENTITY_SECRET"))) // utid_...
mac.Write([]byte(userID))
hash := hex.EncodeToString(mac.Sum(nil))
```

```java Java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(System.getenv("USERTICK_IDENTITY_SECRET").getBytes(StandardCharsets.UTF_8), "HmacSHA256")); // utid_...
String hash = HexFormat.of().formatHex(mac.doFinal(userId.getBytes(StandardCharsets.UTF_8)));
```

```csharp .NET theme={null}
using System.Security.Cryptography;
using System.Text;

var secret = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("USERTICK_IDENTITY_SECRET")!); // utid_...
var hash = Convert.ToHexString(HMACSHA256.HashData(secret, Encoding.UTF8.GetBytes(userId))).ToLowerInvariant();
```

### Check your code

To check your code before any response depends on it, run it with this key and these user IDs. It should give exactly these hashes.

| Secret key | User ID | Hash |
| - | - | - |
| `utid_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8` | `user_123` | `9ea14bd9bc932efbd99d9560af4d0cad9a4117cabe8cb6b2c47f19ed54ca1a17` |
| `utid_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8` | `müşteri_42` | `3537fe65a1328efc03c987f02458d794d905885e2b58189bdf47fd285d60db18` |

The second row checks that the user ID is read as UTF-8. With your own key, enter a user ID and its hash under **Test a hash** in Settings and select **Check hash**. Usertick checks them the way it checks a response and keeps nothing.

## Pass the hash to the page

Where your server renders the page, put the user's ID and hash in it, and pass the hash to `identify()` as `{ hash }`:

```html theme={null}
<script>
  function identify() {
    Usertick.identify("user_123", { plan: "pro", email: "maya@usertick.com" }, { hash: "HASH_FROM_YOUR_SERVER" });
  }
  if (window.Usertick) identify();
  else window.addEventListener("load", identify);
</script>
```

The widget keeps the hash with the user ID until `logout()`. It checks only that the hash is 64 hex characters and logs a warning in the console when it isn't. The other rules for `identify()` are in [Identify your users](https://docs.usertick.com/installation/identify.md).

With a valid hash, the user ID counts as verified, and so do the email, name, company and traits sent with it, because they came from the page that had the user's hash.

## Choose a mode

The mode decides what happens to a response that arrives without a valid hash. A response is never refused because of its identity.

| Mode | Without a valid hash | Results |
| - | - | - |
| **Off** (default) | The identity is kept and marked unverified | Not counted |
| **Monitor** | The same, and its email never reaches a CRM, a mailing list or an event tool | Every hash is checked, and each failure is listed with its reason |
| **Enforced** | The user ID, email, name and traits are dropped. The answers and the anonymous ID are kept | Every hash is checked, and each failure is listed with its reason |

Start with **Monitor**, fix what the results list, then select **Switch to Enforced**. **Enforced** requires a current secret key. Changing the mode asks an account with two-factor authentication for its code.

## Read the results

In **Monitor** and **Enforced**, the **Check the results** step counts every response that came with an identity and lists the newest 50 failures with the time, the reason, how to fix it and the survey. The counts refresh every 30 seconds while the page is open, and **Clear log** starts them over after a fix.

| Reason | What it means | Fix |
| - | - | - |
| **Verified** | The hash matches your key | Nothing to fix |
| **No hash sent** | A user ID or traits came without a hash | Pass `{ hash }` as the third argument of `identify()` |
| **Not a valid hash** | The value isn't 64 hex characters | Send the value your server computed, unchanged |
| **Doesn't match your key** | No active key gives this hash for this user ID | Compute it from the user ID exactly as passed, with the secret key as shown, `utid_` included |
| **No secret key set up** | The organization has no active key | Create a secret key |

The results never hold a hash, a user ID or a trait. They are cleared 30 days after the most recent entry.

## Rotate the key

Rotate the key when it may have leaked, or on your own schedule. Every user's hash changes with the key.

**1. Create the new key**

Select **Rotate key**, then **Rotate now**, and copy the new key.

**2. Deploy it**

Put the new key in your server's environment and compute new hashes with it. Hashes made with the previous key keep verifying for 24 hours, so nothing fails in between.

**3. End the previous key**

To stop the previous key before its 24 hours end, for example after a leak, select **Delete now** on it and type how it starts to confirm.

Rotating again within the 24 hours ends the older key at once, so at most two keys are active. Creating, rotating and deleting a key asks an account with two-factor authentication for its code.

## Verify link survey readers

A [link survey](https://docs.usertick.com/channels/link.md) takes the identity from its address. When your system builds the link for a person, add `uid=user_123&uh=HASH` with that person's hash. The hash never expires, so the link can go out by email or on paper. The page removes both parameters from the address bar once it has read them. A link with `uid` and no `uh` arrives unverified.

## Questions

> [accordion] Is it safe to put the hash in the page?
>
> Yes. The page belongs to the signed-in user, and the hash proves only that user's ID. The secret key can't be worked out from a hash, and a hash doesn't work for any other user ID.

> [accordion] What happens if someone copies a user's hash?
>
> They can send responses as that user, much like someone using that user's signed-in browser. To make every existing hash stop working, rotate the key and select **Delete now** on the previous one.

> [accordion] Why doesn't the hash expire?
>
> A hash that never changes can be computed once and kept with the user, and it works in links sent by email. When you need every hash replaced, rotate the key.

> [accordion] Does Usertick keep the hash?
>
> No. A response keeps whether its hash verified. **Test a hash** and the results keep no hash either.

> [accordion] Does verification change what the visitor sees?
>
> No. The widget behaves the same with or without a hash, and a response is never refused because of its identity.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.