[relaybell docs › Developers › Identity verification](https://relaybell.com/docs/identity) · [All docs as Markdown](https://relaybell.com/docs.md) · [Index for AI assistants](https://relaybell.com/llms.txt)

# Identity verification

> Identity verification: userHash = HMAC-SHA256 of the externalId with the website's secret key; /api/v1/identity.

Stops someone pretending to be one of your users to get their notifications.

The browser SDK tells relaybell who a visitor is with `externalId` (your own user id, often an email). That call is public, so without a check anyone could send *your customer's* `externalId` from their own browser and start receiving that customer's notifications — cart reminders, order updates and anything else you send to that id. With identity verification, your server signs each `externalId` with a secret key that only you and relaybell know, and relaybell only accepts an `externalId` whose signature is correct.

**Turn it on:** in the dashboard, **Settings → Developers → User ID verification** shows your secret key and a **Require verified User IDs** switch. Start sending `userHash` first, then switch it on. The WordPress plugin (1.2.4 or newer) does all of this automatically, and the Shopify code on the dashboard's Install page includes the signature.

## The signature (`userHash`)

`userHash` = lowercase hex **HMAC-SHA256**, where the key is your secret key — the 64-character hex string, used as plain text (don't hex-decode it) — and the message is the `externalId` exactly as you send it (numbers as their text form, e.g. `42` → `"42"`). relaybell compares it without regard to upper/lower case. Make it on your server — never put the secret key in code that runs in the browser.

```js tab="Node.js"
// Node.js
const crypto = require("crypto");
const userHash = crypto.createHmac("sha256", IDENTITY_SECRET).update(String(externalId)).digest("hex");
// or, with the Node SDK: const { userHash } = require("relaybell"); userHash(IDENTITY_SECRET, externalId)
```

```php tab="PHP"
<?php // PHP
$userHash = hash_hmac('sha256', (string) $externalId, $identitySecret);
// or, with the PHP SDK: Relaybell\Client::userHash($identitySecret, (string) $externalId)
```

```python tab="Python"
# Python
import hmac, hashlib
user_hash = hmac.new(IDENTITY_SECRET.encode(), str(external_id).encode(), hashlib.sha256).hexdigest()
# or, with the Python SDK (1.1.0 or newer): from relaybell import user_hash; user_hash(IDENTITY_SECRET, external_id)
```

On **Shopify**, Liquid runs on Shopify's servers, so the key can go in your theme without reaching the page:

```liquid
{{ customer.email | hmac_sha256: 'YOUR_SECRET_KEY' }}
```

## Pass it to the browser SDK

```js
RelayBell.init({
  projectId: "YOUR_PROJECT_ID",
  apiBase: "https://relaybell.com",
  externalId: "jane@example.com",
  userHash: "<signature made on your server>"
});

// or later, e.g. once the visitor signs in:
RelayBell.identify("jane@example.com", "<signature made on your server>");
```

The SDK sends `userHash` wherever it sends `externalId`. If relaybell answers that the id wasn't verified, the SDK logs one console warning: `relaybell: the User ID was not verified — check userHash`.

## What relaybell does with it

| Request | Result |
| --- | --- |
| Valid `userHash` | The `externalId` is used. Response `identity: "verified"`. |
| Missing or wrong `userHash` while verification is **required**, or a wrong `userHash` at any time | The `externalId` is ignored for that request: the browser still subscribes (`201`/`200` as usual) without that id — it isn't linked to the subscriber who has that `externalId`, no stored user id is set or changed, and that subscriber's earlier opt-out isn't cleared (a browser relaybell already knows stays with its own subscriber). Response `identity: "unverified"`. |
| No `userHash` while verification is **not** required (the default) | The `externalId` is accepted as before. Response `identity: "unchecked"`. |

This applies to the public routes the browser SDK uses: `POST /api/v1/projects/:id/subscriptions` (body fields `externalId`, `userHash`; the `identity` field is left out when no `externalId` was sent) and `POST /api/v1/projects/:id/events` (an event's user id is stored only when its `userHash` is valid, or when no hash is sent and verification isn't required). Calls made with your API key — such as `POST /api/v1/events` from your server — are already trusted and don't need `userHash`.

## Managing the secret key

| Route | Description |
| --- | --- |
| **GET** `/api/v1/identity` | Returns `{ "secret": "…", "required": false }`. Creates the secret key on the first call. API key or dashboard session. |
| **PUT** `/api/v1/identity` | Body `{ "required": true }` turns the requirement on (and creates the secret key if there isn't one yet); `false` turns it off. Returns `{ "required": true }`. Anything other than `true`/`false` answers `400 invalid_required`. Turning it **on** works with an API key or the dashboard session; turning it **off** only from the dashboard — with an API key, `{ "required": false }` answers `403 identity_off_requires_dashboard`, so a leaked key (a WordPress site's, say) can't switch the protection off. |
| **POST** `/api/v1/identity/rotate` | Makes a new secret key and returns `{ "secret": "…" }`. The old key stops working at once, so update your site straight away. Dashboard session only — an API key gets `403 session_required`, so a leaked API key can't swap your secret. |

```bash
curl https://relaybell.com/api/v1/identity -H "Authorization: Bearer rb_live_xxxxx"

curl -X PUT https://relaybell.com/api/v1/identity \
  -H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
  -d '{ "required": true }'
```

> **Note:** The secret key is never included in `/config`, webhooks or any other response — only in `/api/v1/identity`. The `identityVerification` value in `GET /api/v1/settings` is read-only: `PUT /api/v1/settings` ignores it, so use `PUT /api/v1/identity`.
