Link a product to the help desk

For the product's developer — or Claude Code working on it. Follow this page from top to bottom.

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_idRequired. Lowercase letters, digits and - (at most 40). Your product's id here; also HELPDESK_APP in your .env.
nameRequired. What the AI and the pages call your product.
descriptionOne line for the AI: what the product is.
guide_urlRequired. 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_nameWhat the guide is called ("user guide", "staff guide").
support_nameWho answers reports ("Stock Counter support").
instructionsRequired (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_labelWhat one customer of your product is called ("Company", "Client", "Store").
timezoneYour users' time zone (Asia/Manila, UTC…): days, daily limits, report times.
github_repoowner/repo where reports become issues (the owner's GitHub token must reach it). Leave out = reports are kept on the help desk only.
originsThe page origins that will show the widget, e.g. ["https://stock.example.com"] (no path). Leave out = any page with a valid token.
contactWho the help desk's owner can ask about this product.
passwordRequired. The linking password from the help desk's owner.
replacetrue 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
appYour app_id (HELPDESK_APP).
tenantThe customer this user belongs to (a company, store…): [a-z0-9._-], at most 63. Conversations, reports and daily limits are per tenant.
groupA 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.
wholetrue when this user may see the past conversations of the whole tenant (administrators). An empty group always does.
role, role_namee.g. "manager" / "warehouse manager". role: [a-z0-9_-].
labelShort "where" for report titles, e.g. "acme/north".
placeOptional 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.
contextText 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, expIssued / 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

TEST SITE