What you get
A round Help chat bubble on your product's pages. Your users ask how to do things and an AI
(Claude) answers only from your user guide; they can report problems, which become GitHub issues
for the developer, and see the answers to their reports. The help desk at https://helpdesk-test.cirms.ph
runs the chat, keeps the conversations and has the owner's dashboard. Your product only tells it
who is asking, with a short signed token. No passwords or user accounts are shared.
Two steps: 1. link your product with the linking password (the help desk's owner gives it to you) — you get your secret at once; 2. add a token endpoint and the widget to your product.
Claude Code: ask your user for the linking password (never guess it, never save it anywhere), show
them the details you are about to send (step 1) and send them when they agree. Never put the secret in code or in the
repository: it goes in the product's .env (or its usual secret store). If your project's rules say not to
write .env, give the value to your user to put there.
1. Link your product
Send your product's details with the linking password. The product is linked at once and the answer holds its
secret — shown only this once: put it in .env as HELPDESK_SECRET right away.
POST https://helpdesk-test.cirms.ph/api/connect
Content-Type: application/json
{
"password": "the linking password",
"app_id": "stock-counter",
"name": "Stock Counter",
"description": "a phone app for counting stock in warehouses, with an admin page",
"guide_url": "https://stock.example.com/help/guide.html",
"guide_name": "user guide",
"support_name": "Stock Counter support",
"instructions": "Users are warehouse staff (role counter) and managers (role manager)...",
"tenant_label": "Company",
"timezone": "Asia/Manila",
"github_repo": "acme/stock-counter",
"origins": ["https://stock.example.com"],
"contact": "Ana, ana@example.com"
}
→ 201 {"app": "stock-counter", "helpdesk_url": "https://helpdesk-test.cirms.ph", "secret": "…64 characters…", "replaced": false, "next": "…"}
400 {"detail": "Please check these fields", "errors": {"guide_url": "…"}}
401 wrong linking password 403 linking is switched off on this help desk
Linking again — to change the details (a new guide address, better instructions…) or when the secret was lost:
send the same request with "replace": true. The details are updated and there is a new secret
(the old one stops working at once), so update HELPDESK_SECRET too. Without "replace",
an id that is already linked is refused — never replace an id that isn't your product's.
| Field | |
|---|---|
app_id | Required. Lowercase letters, digits and - (at most 40). Your product's id here; also HELPDESK_APP in your .env. |
name | Required. What the AI and the pages call your product. |
description | One line for the AI: what the product is. |
guide_url | Required. Your user guide, reachable by the help desk: one page — HTML (one topic per <section id="…">, headings, lists, tables) or Markdown/text — or, for a guide site with many pages (Docusaurus, MkDocs, VitePress…), its sitemap.xml: every page it lists is read (the main content; menus and tables of contents are left out). It is all the AI knows: it must cover every page, button and setting users can reach. Read again every 10 minutes, so updating it updates the AI. No guide yet? Write one (and serve it) before linking. Parts in another language that repeat the same content can be marked with a class the owner can leave out. |
guide_name | What the guide is called ("user guide", "staff guide"). |
support_name | Who answers reports ("Stock Counter support"). |
instructions | Required (20–8000 characters). Product-specific instructions for the AI: who the users are and their roles; the product's own terms (and their translations if users write in other languages); what users can't change themselves (server, settings only an administrator has…) and who they should ask; where data comes from. Don't repeat the guide. |
tenant_label | What one customer of your product is called ("Company", "Client", "Store"). |
timezone | Your users' time zone (Asia/Manila, UTC…): days, daily limits, report times. |
github_repo | owner/repo where reports become issues (the owner's GitHub token must reach it). Leave out = reports are kept on the help desk only. |
origins | The page origins that will show the widget, e.g. ["https://stock.example.com"] (no path). Leave out = any page with a valid token. |
contact | Who the help desk's owner can ask about this product. |
password | Required. The linking password from the help desk's owner. |
replace | true to link an already linked id again (see above). |
2. Add it to your product
Settings (.env, never in code)
HELPDESK_URL=https://helpdesk-test.cirms.ph
HELPDESK_APP=stock-counter # your app_id
HELPDESK_SECRET= # from step 1; blank = no Help bubble
Add these lines (blank) to your .env.example. With HELPDESK_URL or HELPDESK_SECRET
blank the product must work exactly as before, just without the bubble.
A token endpoint (your server, signed-in users only)
For example GET /api/helpdesk-token → {"url": "https://helpdesk-test.cirms.ph", "token": "…", "expires": 1760000000},
or {"url": null} when not set up. It needs the user's normal sign-in; anyone else gets your usual 401.
The token:
token = "v1." + base64url(JSON(claims)) + "." + base64url(HMAC-SHA256(HELPDESK_SECRET, "v1." + base64url(JSON(claims))))
(base64url without "=" padding; the HMAC is over the exact string "v1.<claims>")
| Claim | |
|---|---|
app | Your app_id (HELPDESK_APP). |
tenant | The customer this user belongs to (a company, store…): [a-z0-9._-], at most 63. Conversations, reports and daily limits are per tenant. |
group | A part of the tenant (branch, team, site) or "": [A-Za-z0-9._-], at most 40. "Past conversations" are shared by everyone in the same group. |
whole | true when this user may see the past conversations of the whole tenant (administrators). An empty group always does. |
role, role_name | e.g. "manager" / "warehouse manager". role: [a-z0-9_-]. |
label | Short "where" for report titles, e.g. "acme/north". |
place | Optional finer place (a device, a room) kept with each question. |
facts | [["Company","Acme"],["Site","North (N1)"],["Page","/admin/"]] — shown at the top of report issues. |
context | Text for the AI about this user right now: role, tenant, where they are, their current settings — whatever helps answer "why does X happen for me". No passwords or secrets. At most 20,000 characters. |
iat, exp | Issued / expires, Unix seconds. One hour is right (at most a day). The widget asks for a new token when one expires. Your server's clock needn't match the help desk's exactly: running ahead is fine, and running behind only makes tokens expire early. |
A token the help desk refuses gets 401 {"detail", "hint"}: hint says what exactly is wrong
(no iat, the signature, the tenant's format…). Check it with POST https://helpdesk-test.cirms.ph/api/status {"token": "…"}.
PHP:
$b64 = fn(string $raw) => rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
$body = 'v1.' . $b64(json_encode($claims, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$token = $body . '.' . $b64(hash_hmac('sha256', $body, getenv('HELPDESK_SECRET'), true));
Node.js:
const crypto = require("crypto");
const b64 = (b) => Buffer.from(b).toString("base64url");
const body = "v1." + b64(JSON.stringify(claims));
const token = body + "." + crypto.createHmac("sha256", process.env.HELPDESK_SECRET).update(body).digest("base64url");
Python:
import base64, hashlib, hmac, json, os
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
body = "v1." + b64(json.dumps(claims, ensure_ascii=False).encode())
token = body + "." + b64(hmac.new(os.environ["HELPDESK_SECRET"].encode(), body.encode(), hashlib.sha256).digest())
The widget (on the pages where users need help, once signed in)
// after sign-in:
const first = await (await fetch("/api/helpdesk-token")).json(); // your endpoint (with your usual auth)
if (first.url) {
await new Promise((ok, fail) => { const s = document.createElement("script");
s.src = first.url + "/widget.js"; s.onload = ok; s.onerror = fail; document.head.append(s); });
let pending = first.token;
window.HelpdeskWidget.mount({
server: first.url,
token: async () => { if (pending) { const t = pending; pending = ""; return t; }
return (await (await fetch("/api/helpdesk-token")).json()).token; },
lang: "en", // or "zh" (简体中文) — the widget's own texts come in both
text: { // optional, in the page's language
subtitle: "Ask how to use Stock Counter, or report a problem.",
intro: "Hi! Ask how to do something. Answers come from an AI assistant: check before changing anything important.",
support: "Stock Counter support",
historyGroup: "Questions asked at this site in the last 90 days.",
historyTenant: "Questions asked anywhere in your company in the last 90 days.",
},
roleNames: { counter: "counter", manager: "warehouse manager" },
// screen: false, // never send what's on the page (turns "Show me" off)
// pageData: false, // the person can never share the page's data (turns "Share this page" off)
});
}
// on sign-out:
if (window.HelpdeskWidget) window.HelpdeskWidget.forget();
Colours: set --hdw-brand, --hdw-brand-dark, --hdw-brand-soft, --hdw-ink,
--hdw-ink-2, --hdw-line, --hdw-bg, --hdw-card, --hdw-danger,
--hdw-font on .hdw-root to match your design. To keep the bubble above a fixed bar at the
bottom of your page, set --hd-lift: 76px (the bar's height) on body while the bar shows.
If your pages have a Content-Security-Policy, allow https://helpdesk-test.cirms.ph in script-src,
style-src and connect-src.
Show me on the screen (nothing to do; markers optional)
With each question the widget sends what can be clicked or typed in on the page: kind and visible label only, never
what is typed in fields. When an answer says where to click, a Show me on the screen button highlights each step.
To make it dependable, add data-help="<name>" to key controls (letters, digits, _ . -, e.g.
data-help="new-item"): they are found again after the page changes, also inside a closed menu. Add
data-help-skip to parts whose link texts are customers' data (a list of names…): nothing inside is sent.
Share this page (nothing to do; one marker for private parts)
When an answer depends on the user's own data, the AI asks to see the page and a Share this page button appears.
Only when the user presses it does the widget send the text the page shows (fields' values and tables too), with that
one question; it is not kept. Passwords, card and one-time-code fields are never read, and long numbers are hidden.
Add data-help-private to anything that must never reach the AI (salaries, private notes…), and
data-help-skip parts are left out too. The help desk never needs an API into your data for this.
Tests
For the token endpoint: it needs sign-in; the signature checks out with the secret (compute the HMAC in the test);
the claims are right for each kind of user (tenant, group, whole, role, context); blank settings give
{"url": null}. Never call the real help desk from tests.
Then restart your product: the Help bubble appears for signed-in users.
Good to know
- The guide is the AI's only source. A question it can't answer ends with "Report this problem" — the answer then belongs in your guide. Whenever your pages change, update the guide.
- Users mark answers Resolved / Not resolved; the owner's dashboard groups similar questions and lists the unresolved ones, to improve the guide and the product.
- Reports become issues in
github_repo. Answer on the issue with a comment starting withReply:(by the repo's owner, a member or a collaborator) — users see it; other comments stay internal. Close as completed → Done, not planned → Declined. - Questions, reports and clicks per tenant per day are limited (100, 20, 500 unless the owner changes them).
- The token endpoint and its secret are the only trust between your product and the help desk: keep the secret out of logs, pages and the repository.