# Signing visitors in the widget

> Hand the chat widget what your server knows about a signed-in visitor, signed so the agent can rely on it.

The chat widget on your site can be given values for the agent's [variables](/guides/variables).
Values a page collects on its own are whatever the visitor typed into the address bar; values your
**server** signs are ones the agent can act on.

## How it works

1. Under **Trusted visitor details** on the agent's **Connect → Website widget** tab, choose
   **Create a signing secret**. Keep it on your server.
2. For each page load, your server signs the values it knows to be true — a name, a customer id, a
   plan — into a short-lived token.
3. The page hands the token to the widget: `Aigently.identify(token)`, or a `data-identity` line on
   the widget's snippet.
4. The platform checks the signature before the values reach the agent. A value from a token is
   recorded as `identity_token` in the conversation's `variable_sources`.

## Sign a token

The token is `v1.<payload>.<signature>`: the payload is JSON — `{"exp": <unix time>, "v": {…}}` —
in base64url, and the signature an HMAC-SHA256 of `v1.<payload>` with the secret, also base64url.
Here it is in your language:

**curl**

```sh
#!/bin/sh
# Sign an identity token by hand with openssl, to try the chat widget before your server does it.
# The secret is in AIGENTLY_IDENTITY_SECRET; the token lives for five minutes.
set -eu
base64url() { base64 | tr -d '\n=' | tr '+/' '-_'; }

expires=$(($(date +%s) + 300))
body="$(printf '{"exp":%s,"v":{"first_name":"Sara","customer_id":"C-1042"}}' "$expires" | base64url)"
signature="$(printf 'v1.%s' "$body" \
  | openssl dgst -sha256 -hmac "$AIGENTLY_IDENTITY_SECRET" -binary | base64url)"
echo "v1.$body.$signature"
```

**JavaScript**

```js
// Sign what your server knows about a visitor, for the chat widget. Mint one per page load and
// hand it to the page, which passes it on with Aigently.identify(token) or data-identity.
import { createHmac } from "node:crypto";

// `values` are strings, at most 20 of them. A token lives for at most an hour.
export function signIdentity(values, secret, lifetimeSeconds = 300) {
  const payload = { exp: Math.floor(Date.now() / 1000) + lifetimeSeconds, v: values };
  const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
  const signature = createHmac("sha256", secret).update(`v1.${body}`).digest("base64url");
  return `v1.${body}.${signature}`;
}

console.log(
  signIdentity({ first_name: "Sara", customer_id: "C-1042" }, process.env.AIGENTLY_IDENTITY_SECRET),
);
```

**Python**

```python
# Sign what your server knows about a visitor, for the chat widget. Mint one per page load and
# hand it to the page, which passes it on with Aigently.identify(token) or data-identity.
import base64
import hashlib
import hmac
import json
import os
import time

def _b64url(raw: bytes) -> str:
    return base64.urlsafe_b64encode(raw).decode().rstrip("=")

def sign_identity(values: dict[str, str], secret: str, lifetime: int = 300) -> str:
    """`values` are strings, at most 20 of them. A token lives for at most an hour."""
    payload = json.dumps({"exp": int(time.time()) + lifetime, "v": values}, separators=(",", ":"))
    body = _b64url(payload.encode())
    signature = hmac.new(secret.encode(), f"v1.{body}".encode(), hashlib.sha256).digest()
    return f"v1.{body}.{_b64url(signature)}"

if __name__ == "__main__":
    print(
        sign_identity(
            {"first_name": "Sara", "customer_id": "C-1042"},
            os.environ["AIGENTLY_IDENTITY_SECRET"],
        )
    )
```

**Go**

```go
// Sign what your server knows about a visitor, for the chat widget. Mint one per page load and
// hand it to the page, which passes it on with Aigently.identify(token) or data-identity.
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"time"
)

// SignIdentity signs values, which are at most 20 strings. A token lives for at most an hour.
func SignIdentity(values map[string]string, secret string, lifetime time.Duration) (string, error) {
	payload, err := json.Marshal(struct {
		Exp    int64             `json:"exp"`
		Values map[string]string `json:"v"`
	}{time.Now().Add(lifetime).Unix(), values})
	if err != nil {
		return "", err
	}
	body := base64.RawURLEncoding.EncodeToString(payload)
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte("v1." + body))
	return "v1." + body + "." + base64.RawURLEncoding.EncodeToString(mac.Sum(nil)), nil
}

func main() {
	token, err := SignIdentity(
		map[string]string{"first_name": "Sara", "customer_id": "C-1042"},
		os.Getenv("AIGENTLY_IDENTITY_SECRET"),
		5*time.Minute,
	)
	if err != nil {
		panic(err)
	}
	fmt.Println(token)
}
```

**Java**

```java
// Sign what your server knows about a visitor, for the chat widget. Mint one per page load and
// hand it to the page, which passes it on with Aigently.identify(token) or data-identity.
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Main {
    /** `values` are at most 20 strings. A token lives for at most an hour. */
    static String signIdentity(Map<String, String> values, String secret, long lifetimeSeconds)
            throws Exception {
        long expires = System.currentTimeMillis() / 1000 + lifetimeSeconds;
        String variables = values.entrySet().stream()
            .map(entry -> quote(entry.getKey()) + ":" + quote(entry.getValue()))
            .collect(Collectors.joining(","));
        String payload = "{\"exp\":" + expires + ",\"v\":{" + variables + "}}";
        Base64.Encoder base64url = Base64.getUrlEncoder().withoutPadding();
        String body = base64url.encodeToString(payload.getBytes(StandardCharsets.UTF_8));
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signature = mac.doFinal(("v1." + body).getBytes(StandardCharsets.UTF_8));
        return "v1." + body + "." + base64url.encodeToString(signature);
    }

    /** A JSON string. Your JSON library does the same. */
    static String quote(String text) {
        StringBuilder out = new StringBuilder("\"");
        for (char c : text.toCharArray()) {
            if (c == '"' || c == '\\') {
                out.append('\\').append(c);
            } else if (c < 0x20) {
                out.append(String.format("\\u%04x", (int) c));
            } else {
                out.append(c);
            }
        }
        return out.append('"').toString();
    }

    public static void main(String[] args) throws Exception {
        System.out.println(signIdentity(
            Map.of("first_name", "Sara", "customer_id", "C-1042"),
            System.getenv("AIGENTLY_IDENTITY_SECRET"),
            300));
    }
}
```

**C#**

```csharp
// Sign what your server knows about a visitor, for the chat widget. Mint one per page load and
// hand it to the page, which passes it on with Aigently.identify(token) or data-identity.
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

Console.WriteLine(Identity.Sign(
    new Dictionary<string, string> { ["first_name"] = "Sara", ["customer_id"] = "C-1042" },
    Environment.GetEnvironmentVariable("AIGENTLY_IDENTITY_SECRET") ?? "",
    TimeSpan.FromMinutes(5)));

static class Identity
{
    // `values` are at most 20 strings. A token lives for at most an hour.
    public static string Sign(IReadOnlyDictionary<string, string> values, string secret, TimeSpan lifetime)
    {
        // Written field by field, which needs no reflection and so works in a trimmed app too.
        using var payload = new MemoryStream();
        using (var json = new Utf8JsonWriter(payload))
        {
            json.WriteStartObject();
            json.WriteNumber("exp", DateTimeOffset.UtcNow.Add(lifetime).ToUnixTimeSeconds());
            json.WriteStartObject("v");
            foreach (var (name, value) in values)
            {
                json.WriteString(name, value);
            }
            json.WriteEndObject();
            json.WriteEndObject();
        }
        var body = Base64Url(payload.ToArray());
        var signature = HMACSHA256.HashData(
            Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes($"v1.{body}"));
        return $"v1.{body}.{Base64Url(signature)}";
    }

    static string Base64Url(byte[] raw) =>
        Convert.ToBase64String(raw).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
```

**PHP**

```php
<?php
// Sign what your server knows about a visitor, for the chat widget. Mint one per page load and
// hand it to the page, which passes it on with Aigently.identify(token) or data-identity.

function aigently_base64url(string $raw): string
{
    return rtrim(strtr(base64_encode($raw), "+/", "-_"), "=");
}

/** `$values` are at most 20 strings. A token lives for at most an hour. */
function aigently_sign_identity(array $values, string $secret, int $lifetime = 300): string
{
    // An object even when empty: json_encode writes an empty array as [].
    $payload = json_encode(
        ["exp" => time() + $lifetime, "v" => (object) $values],
        JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
    );
    $body = aigently_base64url($payload);
    $signature = hash_hmac("sha256", "v1.$body", $secret, true);
    return "v1.$body." . aigently_base64url($signature);
}

echo aigently_sign_identity(
    ["first_name" => "Sara", "customer_id" => "C-1042"],
    (string) getenv("AIGENTLY_IDENTITY_SECRET"),
), "\n";
```

## Rules

- **Values are text**, at most 20 of them. A value longer than 500 characters is cut.
- **A token lives for at most an hour.** Mint one per page load, five minutes is plenty. A token
  claiming to live longer is refused.
- **A signature proves where the values came from, not who is at the keyboard.** A page that signs
  the wrong person's name signs the wrong person's name. Treat these as personalisation, backed by
  your own login.
- **`phone` is special**: a signed `phone` tells the platform who the visitor is, so their chats join
  the history of calls from that number.
