# Notifications: identity verification

> Prove which user a browser is asking for, with an HMAC of the user id signed by your server, so nobody can read another user''s notifications.

A publishable key sits in your page's code, so anyone can copy it. Without a check, anyone holding it could ask for any `user_id`'s notifications, or mark them read. Identity verification closes that: your server signs the signed-in user's id, and Launch answers a browser only for the user id it can prove.

## How it works

1. Create the project's **signing secret** once, and keep it on your server.
2. When your server renders a page for a signed-in user, it computes `user_hash = HMAC-SHA256(signing secret, user_id)` as lowercase hex and hands the page both the user id and the hash.
3. The page sends `user_id` with every bell call, and the hash in the `X-Nordva-User-Hash` header (or a `user_hash` query parameter, or `user_hash` in the JSON body of the two `PATCH` calls).

Launch computes the same HMAC and compares the two in constant time. A missing or wrong hash answers `USER_HASH_INVALID` (403). The hash never expires: it is the same for a user id as long as the secret is.

Calls with the **secret key** from your server need no hash.

## Create the signing secret

From the dashboard, or with the secret key:

```bash
curl -X POST https://api.nordva.dev/v1/notifications/signing-secret \
  -H "Authorization: Bearer nv_live_YOUR_SECRET_KEY"
```

```json
{ "data": { "signing_secret": "nv_ntf_3f9c...", "created_at": "2026-09-26T10:00:00.000Z", "shown_once": true } }
```

The secret is shown **once**, in that answer. Save it where your server keeps secrets (for example `NORDVA_NOTIFICATIONS_SECRET`). Asking again answers `SIGNING_SECRET_ALREADY_SHOWN` (409). Never put the secret in the browser or in client-side code: with it, anyone could sign any user id.

The HMAC key is the whole secret exactly as shown, `nv_ntf_` included.

## Compute the hash on your server

**Node.js**

```javascript

  return createHmac("sha256", process.env.NORDVA_NOTIFICATIONS_SECRET).update(userId).digest("hex");
}
```

**Python**

```python

def nordva_user_hash(user_id: str) -> str:
    secret = os.environ["NORDVA_NOTIFICATIONS_SECRET"].encode()
    return hmac.new(secret, user_id.encode(), hashlib.sha256).hexdigest()
```

**PHP**

```php
<?php
function nordva_user_hash(string $userId): string {
    return hash_hmac('sha256', $userId, getenv('NORDVA_NOTIFICATIONS_SECRET'));
}
```

To check your code: with the secret `nv_ntf_example_secret` and the user id `user_123`, the hash is `14a440bc09d5207e006d772d46234831782df0b61b49423e8ec8d6665a92811a`.

## Send it from the page

```javascript
// userId and userHash came from your server with the page
const res = await fetch(
  `https://api.nordva.dev/v1/notifications/inbox?user_id=${encodeURIComponent(userId)}`,
  {
    headers: {
      Authorization: "Bearer nv_pub_live_YOUR_KEY",
      "X-Nordva-User-Hash": userHash,
    },
  },
);
const { data } = await res.json();
// data.notifications, data.unread_count, data.poll_interval_seconds
```

## Rotate the secret

If the secret may have leaked, rotate it:

```bash
curl -X POST https://api.nordva.dev/v1/notifications/signing-secret/rotate \
  -H "Authorization: Bearer nv_live_YOUR_SECRET_KEY"
```

The new secret is shown once, in the answer. The old one keeps working for **24 hours** (`previous_valid_until`), so you can deploy the new one to your server without the bell failing in between. After that only the new one verifies.

## Projects that had notifications before 26 Sep 2026

Before this check existed there was no way to send a hash, so a project that already held notifications keeps working as before: `require_user_hash` is off, and `GET /v1/notifications/unread` and `PATCH /v1/notifications/{id}/read` still answer a browser without a hash. Those answers carry `meta.identity_verification: "off"` with a warning, and `GET /v1/setup-check` and `GET /v1/notifications/settings` warn too. The newer bell endpoints (`inbox`, `unread/count`, `read-all`) always need the hash.

While it is off, anyone with your publishable key can read any user's notifications. To switch it on:

1. Create the signing secret and add the hash to your pages as above.
2. Check a page works with the hash sent.
3. Switch the check on:

```bash
curl -X PATCH https://api.nordva.dev/v1/notifications/settings \
  -H "Authorization: Bearer nv_live_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "require_user_hash": true }'
```

Every other project has the check on from the start.

Source: https://docs.nordva.dev/guides/notifications-identity/
