Who this is for. The person who runs your Sonela workspace does sections 1 to 4 and 8 to 11 in the dashboard. Sections 5 to 7 are for whoever maintains your website — hand them this page. Nothing here needs a Sonela account except the dashboard steps, and nothing here is secret except the one value section 4 tells you to keep that way.
- What you get, and what you do not
- Before you start
- The settings that need no code
- Generate the signing secret
- Sign every page your server renders
- Put the two values on the script tag
- A site of static files
- Check it before you switch it on
- Switch on signed pages only
- When it does not work
- Rotating the secret
- What to watch afterwards
- The checklist
1. What you get, and what you do not
The public assistant's key is public by design: it sits in your page's source, and a copy of it works from anywhere. Everything that constrains it is on our side — which websites it may appear on, how many questions a day, and the fact that a public session never reaches your database, cannot read a visitor's screen and cannot take a picture of it. This guide adds the one control that is yours: a signature only your own server can produce, put on the page as it is rendered, and checked before the assistant opens a session.
Be exact about what that buys, because it is easy to oversell:
- The allowed-origins list stops other browsers, not other programs. A browser honours it; a script carrying your key sends whatever origin it likes.
- The signature survives a script. A signature the script does not hold cannot be forged. But the signature travels in the same page as the key, so anything that can fetch your page gets both. What the signature does is put a copy on a clock: it expires within hours, so a copied key stops working unless something keeps scraping your site — and that is traffic you can see.
- The daily cap bounds what anyone can spend. Whatever else happens, the assistant stops at the number you set and tells visitors it is unavailable until tomorrow.
-
Switching signed pages on closes the public link and the app for this key.
The page at
app.sonela.ai/t/…/assistantis ours, and your server did not sign it, so it refuses. If you hand that link out, this is not the setting for you.
What it answers from is what you opened to visitors: the part of your business context written for them, the manuals you made public, your website, your social posts and, for a shop, your product catalog. A product it recommends is drawn under the answer as a card with its picture, price and a link to its page; the product catalog guide says how to give it yours.
2. Before you start
- The public assistant is already on: Widget → Public assistant shows "The public assistant is on" and the tag to paste.
- Your website renders its pages on a server you control — PHP, Node, Python, .NET, Java, anything that runs code per request — or it is static files on a host that offers a small server-side function beside them (section 7). A site that is only files, with nothing beside them, cannot do this; leave the setting off.
- Somewhere on that server to keep one secret out of the page source: an environment variable, your host's application settings, a secrets store. Not a file in the web root, and never the HTML.
- The page that carries the assistant is not cached for more than a few hours by a CDN or a static-page cache. A signature is good for eight hours; a page cached for a day would go on presenting a dead one.
3. The settings that need no code
Open Widget → Public assistant. Under Settings:
-
Websites it may appear on. The exact origins that serve the page,
comma-separated:
https://www.example.com, https://example.com. An origin is scheme and host, with a port only if it is not the default — no path, no trailing slash, no wildcard. The dashboard refuses anything else as you type. If visitors reach the site both with and withoutwww, list both, or the one you leave out silently gets no assistant. - Questions a day. Set it to what your busiest day genuinely needs, between 1 and 5,000. Past it, visitors are told the assistant is unavailable until tomorrow; nothing else changes. It is the ceiling on what anyone at all can spend of your monthly quota through this key.
- Page loads that show it. 100% puts the bubble on every page load. Lower it to show the assistant to fewer visitors on a busy site: each full page load draws on its own, so a visitor who does not get the bubble may get it on a later one. On a site that changes pages without reloading, one load is the whole visit. It is your website alone; the link and the app always open.
- Name. What the assistant calls itself to visitors. Nothing to do with security, but it is on the same screen.
- Press Save settings.
4. Generate the signing secret
Open Widget → Security. Under Signing secret, press Generate signing secret. It is shown once, in an amber sheet, and never again: Sonela keeps a protected copy it can verify against but not read back. Copy it now.
It begins sk_ and is one line. The whole line, prefix included, is the key
your server signs with in the next section — do not strip the prefix.
The secret goes into your server's environment and nowhere else.
Name it SONELA_SIGNING_SECRET so the samples below work unchanged. If it
ever appears in page source, a repository, a ticket or a chat, rotate it (section
11).
5. Sign every page your server renders
On every page that carries the assistant, at the moment your server renders it, compute two values and put them on the script tag (section 6). The rules are exact, because a signer that hashes something slightly different does not fail loudly — the assistant simply never opens.
| Value | Rule |
|---|---|
timestamp |
The current time in whole Unix seconds, as a string of digits and nothing else — no sign, no decimals, no milliseconds. Sonela accepts it up to 30 seconds ahead of its own clock and up to 8 hours behind, so keep your server's clock right. |
| what is signed |
The text page: followed by the timestamp — for example
page:1788091200. Nothing before, nothing after, no spaces.
|
| how |
HMAC-SHA256, with the secret as the key: the whole
sk_… line, as UTF-8 bytes.
|
signature |
The 32-byte result as 64 hexadecimal characters. Upper or lower case both verify; the samples produce lower case. |
| when | Per render. Never a value computed once and reused, never one written into a static file — it would expire eight hours later and every page would go dark. |
Check your signer against this vector first
With the secret sk_ followed by 64 letter as and the timestamp
1788091200, the signature must be exactly:
e5d7708b944c600435647b60538f9027cee1516ef6f5ab56fdd964853b09bdac
Run your code once with those two inputs. If it prints anything else, the preimage, the key or the encoding is wrong, and switching the setting on would stop your assistant. Once it matches, swap in the real secret and the real time.
Node.js
const crypto = require('crypto');
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac('sha256', process.env.SONELA_SIGNING_SECRET)
.update('page:' + timestamp)
.digest('hex');
PHP
$timestamp = (string) time();
$signature = hash_hmac('sha256', 'page:' . $timestamp, getenv('SONELA_SIGNING_SECRET'));
Python
import hashlib, hmac, os, time
timestamp = str(int(time.time()))
signature = hmac.new(
os.environ["SONELA_SIGNING_SECRET"].encode("utf-8"),
f"page:{timestamp}".encode("utf-8"),
hashlib.sha256,
).hexdigest()
C# (.NET 6 or later)
using System.Security.Cryptography;
using System.Text;
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var signature = Convert.ToHexString(HMACSHA256.HashData(
Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SONELA_SIGNING_SECRET")!),
Encoding.UTF8.GetBytes("page:" + timestamp)))
.ToLowerInvariant();
Java (17 or later)
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
String timestamp = Long.toString(Instant.now().getEpochSecond());
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(
System.getenv("SONELA_SIGNING_SECRET").getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = HexFormat.of().formatHex(
mac.doFinal(("page:" + timestamp).getBytes(StandardCharsets.UTF_8)));
6. Put the two values on the script tag
Take the tag from Widget → Public assistant — "Paste into your website" — and add exactly two attributes. Everything else stays as the dashboard printed it:
<script src="https://api.sonela.ai/sonela.js"
data-key="pk_…"
data-page-timestamp="1788091200"
data-page-signature="e5d7708b944c600435647b60538f9027cee1516ef6f5ab56fdd964853b09bdac"
defer></script>
Your template fills data-page-timestamp and
data-page-signature from the two values your server just computed. Do not
add any other attribute: the assistant's name, colour, greeting and corner are settings
in the dashboard, and a tag that configures them from the page is configuring the
assistant from the one place a visitor can edit.
One exception, for a page people keep open: a signature is good for eight hours, and a
tab left open longer than that cannot start a new conversation on the signature it was
rendered with. If your pages stay open for hours — a web application rather than a
brochure — also serve the two values as JSON from a path on your own domain, in the
shape section 7's function answers, and name that path in
data-vouch-url="/api/sign-page" on the tag. The assistant then asks it for
a fresh signature before each session it starts, and falls back to the two attributes
when the path does not answer in time.
7. A site of static files
A site with no server has nothing to sign with — unless its host runs a small function beside the files, as Azure Static Web Apps, Netlify, Cloudflare Pages and Vercel all do. Then the shape changes in one way: the page cannot carry the values, so a tiny loader asks the function for them and adds the tag itself. Sonela's own website works exactly like this, and both halves are short.
The signing function
One HTTP endpoint on your site's own domain, say /api/sign-page, answering
GET with JSON and a Cache-Control: no-store header. The secret
is the host's application setting SONELA_SIGNING_SECRET. Shown for Azure
Static Web Apps (Node 22, the @azure/functions v4 package); every other
host's function is the same twenty lines in its own wrapper.
const crypto = require("crypto");
const { app } = require("@azure/functions");
app.http("sign-page", {
methods: ["GET"],
authLevel: "anonymous",
route: "sign-page",
handler: async () => {
const secret = process.env.SONELA_SIGNING_SECRET;
const headers = { "Cache-Control": "no-store" };
if (!secret) {
return { status: 503, headers, jsonBody: { error: "no signing secret on this host" } };
}
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac("sha256", secret.trim())
.update("page:" + timestamp)
.digest("hex");
return { status: 200, headers, jsonBody: { timestamp, signature } };
},
});
The loader
Replace the assistant's tag on every page with
<script src="/assets/assistant.js" defer></script>, and put this in
that file, with your own key on the first line:
(function () {
"use strict";
var KEY = "pk_…";
var RUNTIME = "https://api.sonela.ai/sonela.js";
var SIGNER = "/api/sign-page";
var TIMEOUT_MS = 8000;
function isSignature(v) {
return !!v && typeof v.timestamp === "string" && /^[0-9]{1,12}$/.test(v.timestamp)
&& typeof v.signature === "string" && /^[0-9a-f]{64}$/.test(v.signature);
}
function mount(signed) {
var tag = document.createElement("script");
tag.src = RUNTIME;
tag.dataset.key = KEY;
if (signed) {
tag.dataset.pageTimestamp = signed.timestamp;
tag.dataset.pageSignature = signed.signature;
}
tag.dataset.vouchUrl = SIGNER;
document.head.appendChild(tag);
}
if (typeof window.fetch !== "function" || typeof window.AbortController !== "function") {
mount(null);
return;
}
var settled = false;
var deadline = new window.AbortController();
var timer = window.setTimeout(function () { deadline.abort(); }, TIMEOUT_MS);
function settle(signed) {
if (settled) return;
settled = true;
window.clearTimeout(timer);
mount(signed);
}
window
.fetch(SIGNER, { cache: "no-store", signal: deadline.signal })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (s) { settle(isSignature(s) ? s : null); }, function () { settle(null); });
})();
Three things about it are deliberate. It mounts the assistant unsigned
when it cannot get a signature — the function is missing, has no secret yet, or is
slow — so deploying the loader before section 9 changes nothing; whether an unsigned
page may answer is the dashboard's decision, not the loader's. It fetches with
no-store, because a signature is a fact about now. And it names the
function in data-vouch-url, so the assistant asks it for a fresh signature
before each session it starts: a signature is good for eight hours, and a page a
visitor leaves open longer than that would otherwise have to be reloaded before it
could answer again.
8. Check it before you switch it on
Sonela checks the signature only once the setting is on, so these checks are yours, and they take five minutes:
- The vector matched (section 5). If you skipped it, do it now — it is the one check that catches a wrong preimage before a visitor does.
-
The page carries both values. Open a page that has the assistant,
view its source (or, with the loader of section 7, the browser's developer tools →
Elements, where the mounted tag appears in
<head>). The tag hasdata-page-timestampwith ten digits anddata-page-signaturewith 64 hexadecimal characters. - They are fresh. Reload twice a few seconds apart: the timestamp moves, and the signature with it. If either stays the same, a cache is serving an old render — the page must be rendered, or the function called, per request.
-
The timestamp is now. Compare it with the current Unix time
(
date +%son a Mac or Linux shell,[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()in PowerShell). More than 30 seconds ahead, or hours behind, and the server's clock is wrong. - Every page. The assistant is per page, so a page you forgot to sign is a page where it stops. Check one of each template.
9. Switch on signed pages only
- Widget → Public assistant, under Settings, tick Only answer pages my own server signed. A box appears with what your server must output; you did that in section 5.
- Press Save settings.
- Open your website in a private window, open the assistant and ask it something. It answers. That is the whole confirmation.
What changes the moment it is saved:
- A page without a valid signature no longer opens a session. A visitor there who opens the assistant sees "The assistant could not verify this page. Please reload the page and try again." — never the reason, which is on purpose.
- The public link and the Sonela app are closed for this key, and the panel that showed the link says so.
- A visitor who already had the assistant open on your site keeps their session until it expires; the next session they open is checked.
10. When it does not work
| What you see | What it means | What to do |
|---|---|---|
| "The assistant could not verify this page" on your own site, on every page | The signature does not verify: wrong preimage, secret pasted with the prefix stripped, a timestamp in milliseconds, or the secret on the server is not the one the dashboard holds. |
Untick the setting and save, so visitors are served. Run the vector (section
5). Check the environment variable is the whole sk_… line. Turn
it back on.
|
| The same, on one page or one template only | That page renders the tag without the two attributes, or from a cached copy. | Sign that template; make sure it is rendered per request. |
| Fine at first, then failing hours later | A CDN or page cache is serving a render older than eight hours, so its signature has expired. Or the page itself has been open that long: the assistant starts a new session every half hour of use, and each one needs a signature younger than eight hours. |
Cache the page for less than eight hours, or exclude it from the cache. With the
loader of section 7 the page itself may be cached; only the function's answer
must not be. For a page people keep open, name the signing function in
data-vouch-url (sections 6 and 7) so each session fetches a
signature of its own.
|
| Failing on some visitors' machines, not others | Nothing to do with their machines: the check is on your server's clock. | Compare the server's time with a reference. More than 30 seconds fast, or hours slow, is refused. |
| The public link or the app stopped opening | By design while signed pages only is on (section 1). | Untick the setting if you need the link. |
| Rotated the secret, and pages began failing a day later | Rotation keeps the old secret working for 24 hours; the server was still signing with it when the grace ended. | Put the new secret in the server's environment and restart what needs it. |
| The bubble is on some page loads and not on others | Page loads that show it is below 100%, and each load draws on its own. | Set it to 100% if every visitor should get the bubble. |
| The dashboard refuses to save the setting | "Generate a signing secret first": there is no secret to verify against. | Section 4. |
11. Rotating the secret
On Widget → Security, Rotate signing secret issues a new
one and keeps the old one working for 24 hours, so you can update the
server at your own pace: pages signed with either verify until the grace ends, and the
screen shows the deadline while it is open. Update SONELA_SIGNING_SECRET
before then.
If you are rotating because the old secret leaked, press Revoke previous secret right after updating the server. That ends the overlap at once; anything still signing with the old value is refused from that moment.
12. What to watch afterwards
- Activity lists every refused signature as Signed identity refused, folded by hour. A burst right after you switch on is a page you missed; a steady trickle weeks later is someone re-fetching your pages to keep a copied key alive — which is the traffic this setting exists to make visible.
- Widget → Public assistant shows today's questions against the cap. If the cap is reached on ordinary days, raise it; if it is reached on days your site was quiet, look at Activity.
- Usage labels this key's questions Public site, so what the public assistant spends of your quota is one filter away.
13. The checklist
- Origins are the exact ones that serve the page, with and without
wwwas needed. - The daily cap is what a busy day needs and no more.
- The signing secret is generated and stored only in the server's environment.
- The signer reproduces the vector in section 5.
- Every template that carries the assistant signs per render, or the loader and the function are in place.
- The page is not cached for eight hours or more.
- A fresh load shows both attributes, and a second load shows different values.
- The setting is on and saved, and the assistant answers on your site.
- Nobody depends on the public link or the app for this key.