Get started

Identify your users

Tell Meles who is using your app, so a campaign can target them by plan or role and their answers are linked to them. Sign each user ID on your server, so nobody can pretend to be someone else.

Call identify

Once your user has signed in, pass their ID and the traits you want to target on. The third argument carries the hash your server computed for that ID:

Your page
Meles.identify('user_8412', { plan: 'pro', role: 'admin' }, { userHash: hashFromYourServer });
  • The user ID is yours, up to 256 characters. Meles links the visitor’s earlier, anonymous activity to it.
  • Traits are strings (up to 1,000 characters), numbers, booleans or null, under names of up to 128 characters that don’t start with $. Send at most 50 per person, with their company’s included. Later calls add to them and overwrite them.
  • A company goes in the third argument too: { company: { id: 'acme', traits: { plan: 'enterprise' } }, userHash }.
  • When your user signs out, call Meles.reset(). It forgets them and starts a new anonymous ID, so the next person on a shared device isn’t counted as them. Their frequency caps stay with the device.

Sign the user ID on your server

Every site has an identity secret: Settings → Sites, your site, Identity secret. With it, Meles refuses an identify whose userHash is missing or wrong, with 401 ingest.identity_unverified, and stores nothing. So someone who edits your page can’t claim to be another user.

The hash is HMAC-SHA256 of the user ID’s UTF-8 bytes, keyed with the secret’s UTF-8 bytes exactly as issued, written as 64 lower-case hex characters. Compute it on your server and put it in the page with the user ID. The secret never goes in the page. Don’t normalise or trim the user ID: hash exactly the string you pass to identify. The hash is a bearer credential: it never expires, so anyone who has a user’s ID and hash can identify as that user, from any page or script, until you rotate the secret. Treat it like a session token: put it only in that signed-in user’s own page, over HTTPS, and keep it out of URLs, logs, analytics and any cache shared between users.

Node

user-hash.mjsChecked in CI against the test vectors below
import { createHmac } from 'node:crypto';

/**
 * The `userHash` that `Meles.identify()` takes for `userId`: HMAC-SHA256 keyed with the site's
 * identity secret, over the user ID's UTF-8 bytes, as 64 lower-case hex characters. Run it on your
 * server and send the result to the page: the secret never reaches the browser.
 */
export function userHash(userId, identitySecret) {
  return createHmac('sha256', identitySecret).update(userId, 'utf8').digest('hex');
}

Python

user_hash.pyChecked in CI against the test vectors below
import hashlib
import hmac


def user_hash(user_id: str, identity_secret: str) -> str:
    """The userHash that Meles.identify() takes for user_id.

    HMAC-SHA256 keyed with the site's identity secret, over the user ID's UTF-8 bytes, as 64
    lower-case hex characters. Run it on your server and send the result to the page: the
    secret never reaches the browser.
    """
    return hmac.new(identity_secret.encode("utf-8"), user_id.encode("utf-8"), hashlib.sha256).hexdigest()

C#

MelesIdentity.csChecked in CI against the test vectors below
using System.Security.Cryptography;
using System.Text;

/// <summary>Identity verification for Meles: copy this class to your server.</summary>
internal static class MelesIdentity
{
    /// <summary>
    /// The <c>userHash</c> that <c>Meles.identify()</c> takes for <paramref name="userId"/>:
    /// HMAC-SHA256 keyed with the site's identity secret, over the user ID's UTF-8 bytes, as 64
    /// lower-case hex characters. The secret never reaches the browser.
    /// </summary>
    public static string UserHash(string userId, string identitySecret) =>
        Convert.ToHexStringLower(HMACSHA256.HashData(Encoding.UTF8.GetBytes(identitySecret), Encoding.UTF8.GetBytes(userId)));
}

Test vectors

Any language’s HMAC-SHA256 will do. These are the vectors the samples above are checked against on every change to them; the secrets are examples, never real. All four are in identify-hmac.json.

VectorSecretUser IDuserHash
asciiidsec_ExampleSecretForTheHmacVectors0001user_841264e23579487d20128d12d6fe6bd9080de42d0ee822e570feae945c128173dbdd
non-asciiidsec_ExampleSecretForTheHmacVectors0001Zoë Ångström 🦡b66572a427d0aacde76987aecd22238d7c3a3b8f0e2d2dc937615dc8319ef559
decomposedidsec_ExampleSecretForTheHmacVectors0001Zoë Ångström 🦡5b198610e962a757dd473854cfa26aadc2fd1a6f507125b0e7bda8d760e5aaea

The second and third differ only in how the accented letters are encoded, as one character or a letter and an accent, which is why the user ID is hashed exactly as given.

Rotating the secret

Rotate secret on the same card issues a new secret, shown once. For the next 24 hours Meles accepts a hash made with either, so deploy the new one to your servers within that day. Rotating is also how you end a hash that has leaked: it stops working when the 24 hours end, or at once if you rotate a second time, which also refuses any of your servers still hashing with the old secret.

Identifying from your server

A server can identify a person itself, with a secret API key that holds the runtime scope, through the identify_user tool or POST /v1/identify, naming the site. The key proves who is calling, so no userHash is needed. See Connect an AI agent for keys.