Docs

Identity verification

Recognize a signed-in person and restore their conversation history across devices, using a signature your backend controls.

Why verify#

By default the SDK is anonymous: it generates a stable visitor_id and keeps it in the platform keystore. That is enough for one conversation on one device. To recognize a specific user, restore history on relogin or another device, and reliably bind a conversation to a contact profile, the backend needs proof the client is who it claims to be. If the SDK simply sent "I am user 42", anyone could spoof another id and read someone else's chat — so Respondo requires a cryptographic signature, userHash, that only your backend can produce.

Getting the identity secret#

The identity_secret is a secret string bound to your agent (the AI worker your widget channel is connected to). Generate it in the dashboard under Channels → Widget → Identity verification. Because it lives on the agent, every channel backed by that agent — web widget and mobile SDK alike — shares the same secret. It grants the right to sign identities, so it must live only on your backend and must never be shipped in the app.

The userHash formula#

userHash is an HMAC-SHA256 over a single signed identity string, encoded as lowercase hex:

Formulatext
userHash = HMAC_SHA256( identity_secret, payload )

payload = userId          // if userId is set
        = email           // otherwise, if email is set
        = (invalid)       // if both are empty, there is nothing to sign
  • The secret is the HMAC key; the identity string is the message — not the other way round.
  • Sign exactly one string — the raw userId (or email), with no salt or JSON wrapper.
  • If you pass both userId and email, sign the userId (it takes priority).
  • Output is hex (64 characters for SHA-256), not base64.

Backend examples#

Compute the signature on your backend and hand the finished userHash to the app.

Gogo
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

// HMAC_SHA256(identity_secret, userId) as lowercase hex.
func ComputeUserHash(identitySecret, userID string) string {
    mac := hmac.New(sha256.New, []byte(identitySecret))
    mac.Write([]byte(userID))
    return hex.EncodeToString(mac.Sum(nil))
}
Node.jsjavascript
const crypto = require("crypto");

// Returns the userHash to pass into Respondo.identify on the mobile client.
function computeUserHash(identitySecret, userId) {
  return crypto
    .createHmac("sha256", identitySecret)
    .update(userId, "utf8")
    .digest("hex");
}
Pythonpython
import hmac
import hashlib

# Returns the userHash to pass into Respondo.identify on the mobile client.
def compute_user_hash(identity_secret: str, user_id: str) -> str:
    return hmac.new(
        identity_secret.encode(),
        user_id.encode(),
        hashlib.sha256,
    ).hexdigest()
PHPphp
<?php
// Returns the userHash to pass into Respondo.identify on the mobile client.
function computeUserHash(string $identitySecret, string $userId): string {
    return hash_hmac('sha256', $userId, $identitySecret);
}

How the hash reaches Respondo#

Respondo exposes no identity endpoint. There is no /identity route to call and nothing to register. userHash is a field that rides the requests the SDK already makes.

Two hops, and only the first one is yours to build:

  1. Your backend → your app. You deliver the hash however you like. The cheapest option is an extra field on the login/bootstrap response you already return — no additional round trip. A dedicated endpoint on your own backend (say POST /myapp/identity returning { userId, userHash }) works equally well. Respondo does not host that endpoint — you implement it.
  2. Your app → Respondo. Handled for you. Once you call identify, the SDK attaches the hash to every relevant request and the backend re-verifies it each time.
Where the SDK puts it (for reference — you do not send these by hand)text
POST /api/v1/chat                      body   identity.userHash
GET  /api/v1/chat/resume               query  user_hash
GET  /api/v1/chat/history              query  user_hash
GET  /api/v1/chat/ws                   frames    user_hash (subscribe/identify JSON frames after connect — not in the handshake URL)
GET  /api/v1/widget/tours              query  user_hash
GET  /api/v1/widget/checklists         query  user_hash
POST /api/v1/widget/push/register      body   user_hash

The hash is re-checked on every request, not exchanged once for a session — that is what makes a stolen userId useless on its own.

If the delivery endpoint on your backend is not built yet, your app's own request 404s, identify is never called, and the chat runs anonymously. That is expected behaviour, not a Respondo error — a 404 on your own path is a to-do on your side, not a broken integration.

Passing identity to the SDK#

Fetch the signed identity from your backend, then pass the userHash into identify on any platform:

Kotlinkotlin
Respondo.identify(
    RespondoIdentity(userId = "42", email = "user@example.com", userHash = hash),
)
Swiftswift
Respondo.identify(
    RespondoIdentity(userId: "42", email: "user@example.com", userHash: hash)
)
Dartdart
Respondo.identify(RespondoIdentity(
  userId: '42', email: 'user@example.com', userHash: hash,
));

Invalid userHash behavior#

When verification is enabled on the agent and the signature is missing or wrong, the backend performs a silent downgrade to anonymous: the chat still works, the visitor_id is kept, and the conversation is bound to the anonymous contact — but there is no link to the profile and no cross-device history. If a user "is not recognized", it is almost always the signature: check that you signed the userId (not email or JSON), used the right identity_secret, and emitted lowercase hex. If the agent's identity_secret is empty, verification is off and userId / email are accepted as-is.

Rotating the identity_secret is effectively a hard cutover: every hash produced with the old secret stops verifying at once, so already-signed-in users silently fall back to anonymous until your backend recomputes and re-supplies their userHash with the new secret. Roll the secret only when you can update the signing side in the same window.