Install it in an afternoon
One script tag gets the chat on your site. If your visitors sign in, a few lines on your server let your team see exactly who they are talking to.
Add the widget
Everyone starts here. It takes a few minutes and works on any site where you can edit the HTML.
-
Add your site in the console
Give it a name and list the domains the widget may run on. Requests from any other origin are refused, both for the API and for the live connection.
-
Store the two secret keys
The console shows an identity key (sk_…) and a webhook key (whsec_…) once. Put both in your server's environment. They never belong in the browser.
-
Paste the snippet on every page
Place it before the closing body tag. It loads asynchronously, after your page, and renders in a Shadow DOM so your CSS and the widget's never meet.
<script src="https://app.agentlydesk.com/widget.js" data-site-key="pk_live_xxx" async></script> pk_live_… is your site key. It is public by design: the widget can only open on the domains you
listed.
Verify signed-in visitors
Anyone can call identify() and claim to be someone else, so the widget only trusts a name when your server has signed it. Part of the Business plan.
Your server computes an HMAC-SHA256 with the identity key (sk_…) over these
exact bytes, one field per line:
identity.v2
<ts>
<id>
<email>
<name>tsis the signing time in unix seconds.- A missing e-mail or name is signed as an empty string.
- Fields may not contain line breaks.
- Signatures older than 24 hours, or more than 5 minutes in the future, are rejected. Sign again on every page load.
If a signature does not verify, the visitor stays anonymous and the console marks the chat as not verified. A
shared browser never hands one person's conversation to the next: call AgentlyDesk.reset() on
sign-out and the widget starts a clean session.
For members-only support, turn on Signed-in members only for the site. Requests without a valid signature are then refused outright.
import crypto from "node:crypto";
// Run this on every page load for a signed-in user.
const ts = Math.floor(Date.now() / 1000); // unix seconds
const id = String(user.id);
const email = user.email ?? "";
const name = user.name ?? "";
const hash = crypto
.createHmac("sha256", process.env.AGENTLYDESK_IDENTITY_KEY) // sk_…
.update(`identity.v2\n${ts}\n${id}\n${email}\n${name}`)
.digest("hex");
// Pass id, ts, hash, email and name to the page.// Signed in. ts is a number; leave email or name out if the user has none.
AgentlyDesk.identify({ id, ts, hash, email: user.email, name: user.name });
// Signed out, or switched account: start over as a new visitor.
AgentlyDesk.reset();// Before widget.js has loaded, queue the call instead.
window.AgentlyDeskQueue = window.AgentlyDeskQueue || [];
window.AgentlyDeskQueue.push({
method: "identify",
args: [{ id, ts, hash, email: user.email, name: user.name }],
});Receive tickets
When a conversation with a signed-in visitor needs follow-up, an operator can send it to your ticketing system. Part of the Business plan.
Set the site's support ticket address in the console, under Settings, Sites. When an operator turns a chat into a ticket, AgentlyDesk sends a
POST there with a JSON body and two headers:
X-Chat-Timestamp: <unix seconds>
X-Chat-Signature: sha256=<hex>
The signature is an HMAC-SHA256 with the webhook key (whsec_…, never the identity
key) over webhook.v1.<timestamp>.<raw body>. Verify it against the raw bytes, before
parsing the JSON.
- Reject timestamps older than 5 minutes, and signatures you have already seen.
- Compare in constant time.
- While the webhook key is being rotated, the header carries two comma-separated signatures, the new one first. Accept the request if any one matches, so tickets keep arriving until you deploy the new key.
Answer with a 2xx status and the ticket you created. The console shows the ticket number, and a link when you send one.
{ "ticketId": "...", "url": "..." }
{ "ticketId": "...", "url": null }import crypto from "node:crypto";
import express from "express";
const app = express();
// Keep the raw bytes: the signature covers the body exactly as it was sent.
app.post("/agentlydesk/ticket", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-Chat-Timestamp") ?? "";
const signatureHeader = req.get("X-Chat-Signature") ?? "";
const rawBody = req.body.toString("utf8");
// Reject requests older than five minutes.
const age = Date.now() / 1000 - Number(timestamp);
if (!Number.isFinite(age) || Math.abs(age) > 300) return res.sendStatus(401);
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.AGENTLYDESK_WEBHOOK_KEY) // whsec_…
.update(`webhook.v1.${timestamp}.${rawBody}`)
.digest("hex");
// While the key is being rotated, two signatures arrive (newest first).
// Accept the request if any one of them matches.
const ok = signatureHeader.split(",").some((s) => {
const a = Buffer.from(s.trim()), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b); // constant-time
});
if (!ok) return res.sendStatus(401);
// Reject replays: remember signatures for five minutes and refuse repeats.
if (seenRecently(signatureHeader)) return res.sendStatus(409); // your store
const ticket = createTicket(JSON.parse(rawBody)); // your ticketing code
res.json({ ticketId: String(ticket.id), url: ticket.url ?? null });
});{
"event": "ticket.create",
"conversationId": 1042,
"externalId": "user-123",
"subject": "…",
"transcript": "…",
"operatorName": "Maya"
}JavaScript API
The widget puts one object on the page, AgentlyDesk. Calls made before the script has loaded can wait in window.AgentlyDeskQueue.
| Call | What it does |
|---|---|
AgentlyDesk.open() | Open the chat window. |
AgentlyDesk.close() | Close the chat window. |
AgentlyDesk.toggle() | Open it if it is closed, close it if it is open. |
AgentlyDesk.identify({ id, ts, hash, email, name }) | Tell the widget who is signed in, with your server's signature. |
AgentlyDesk.reset() | Forget the current visitor and start a clean session. Call it on sign-out or account switch. |
AgentlyDesk.hide() | Hide the launcher for a while, for example behind an open drawer. |
AgentlyDesk.show() | Show the launcher again. |
AgentlyDesk.on((event) => { … }) | Listen for ready, message, typing, status and error events. |
Running a strict Content Security Policy?
widget.js updates itself, so it cannot carry an integrity hash. Every release also
publishes a versioned copy, with its digest listed in widget.manifest.json. Pin that file when your
policy requires Subresource Integrity, and update it when you choose to.
<script src="https://app.agentlydesk.com/widget.<hash>.js"
integrity="sha384-…" crossorigin="anonymous"
data-site-key="pk_live_xxx" async></script>Key rotation and the other safeguards are described on the security page.
Ready to paste the snippet?
AgentlyDesk is in early access. Tell us about your site and we will set up your workspace and send your keys.