---
name: helpdesky-integration
description: Integrate Helpdesky (help center, support widget, contact form, ticket center, and docs) into this app. Use when the user wants to add a help/support widget, add a knowledge base or help center, add a "contact us"/contact form, let users contact support or see their support tickets, link to documentation, auto-write or sync help articles, route their support email into Helpdesky, host the help center on their own domain (custom subdomain, a subfolder such as /help via a reverse proxy, or headless inside their own templates), or connect an agent to the Helpdesky MCP server. Triggers on phrases like "add a help widget", "embed Helpdesky", "add a support center", "add a contact form", "write help docs", "link to our help articles", "put the help center on our domain", "serve the help center at /help", "proxy the help center", "headless help center", "custom domain for the help center", or "Helpdesky MCP".
---

# Helpdesky Integration

This skill teaches you to integrate **Helpdesky** (https://helpdesky.io) — a hosted help-center and support platform — into the app you are building. Everything here uses embed scripts, public endpoints, the hosted help center, and the content REST API that already exist; you never build or host any of it.

Helpdesky serves the embed scripts with `Access-Control-Allow-Origin: *`, so they work from any domain. If the customer has listed domains under **Allowed Embed Domains** (**Settings → Messages** tab), the customer's domain must be on that list for the widget, contact form and ticket center to load and accept requests. An empty list allows any domain; subdomains of a listed domain are included automatically.

The base URL is `https://helpdesky.io` everywhere below. The canonical copy of this skill is served at `https://helpdesky.io/docs/skill.md`.

---

## If your agent can connect to MCP

Helpdesky has an MCP server at **https://helpdesky.io/mcp** (Streamable HTTP, OAuth: the customer signs in and approves the connection). If you can use MCP tools, connect first — most of the "dashboard setup" below then becomes something you do yourself, as the customer, within their permissions:

- **Create the helpdesk** (`create_helpdesk`) when the customer has none yet.
- **Enable the widget and messaging, set Allowed Embed Domains** (`update_helpdesk_settings` with `widgetEnabled`, `messagingEnabled`, `allowedDomains`; `update_widget_settings` for widget options).
- **Configure every hosting mode** (`get_hosting_status`, `set_custom_domain`, `save_subfolder_hosting` / `check_subfolder_setup` / `activate_subfolder_hosting`, `save_headless_hosting` / `check_headless_setup` / `activate_headless_hosting`, `get_hosting_instructions`).
- **Fetch the embed snippets with the Helpdesk ID already filled in** (`get_embed_snippets`), so you never have to ask for the ID.
- **Write and publish articles** (`create_article`, `publish_article`, categories, images) — no `hdh_` API key needed at all when articles go through MCP.
- **Save the customer's AI provider key** (`update_ai_settings`, owner only, write-only) and verify it (`check_ai_configuration`).

The hosting, embeds, settings and AI tools live in the **Setup** tool set, which is off by default; call `enable_tool_sets` (or ask the customer to enable it on their MCP page) if those tools are not listed. **Growth** holds the SEO and link tools.

Never available through MCP, by design: the **HMAC secret** (ticket center), the **`hdh_` API key**, billing, and passwords. The human copies the HMAC secret from **Embeds → Ticket Center** and (only if you call the REST API directly instead of MCP) the API key from **Settings → API Integration**, then stores them as server-side secrets.

Everything that writes code into the customer's app — embed tags, server-side HMAC signing, the subfolder reverse proxy, headless templates — is still your job; MCP only operates the dashboard. Without MCP, follow the dashboard walkthrough below and ask the customer to click through it.

---

## Step 1 — Gather credentials (ask the customer, or use MCP)

Ask the customer only for what the chosen surface needs, then store each value as a **project secret / environment variable — never hardcode keys, and never ship the HMAC secret or API key to the browser.**

| Surface | What to ask for | Where they find it in the Helpdesky dashboard |
|---|---|---|
| Widget | **Helpdesk ID** (a UUID) | The **Helpdesk ID** box on **Settings → API Integration** or any **Embeds** page (**Copy Helpdesk ID** button) |
| Contact form | **Helpdesk ID** | **Embeds → Contact Form** |
| Ticket center | **Helpdesk ID** + **HMAC secret** | **Embeds → Ticket Center** (generate/copy the HMAC secret under **HMAC Authentication**) |
| Writing/syncing articles by REST | **API key** (starts with `hdh_`) | **Settings → API Integration** (not needed when you write articles through MCP) |
| Hosted help center | **helpdesk slug** and the live help center address | **Help Center → Domain & Address** |
| Inbound email | **slug** + **6-digit code** | **Embeds → Email Forwarding** |

The Helpdesk ID is **not** a secret (it ships in the public embed). The **HMAC secret** and **API key are secrets** — keep them server-side only.

**Finding the Helpdesk ID:** it's a UUID and is the same for every surface. The **Settings → API Integration** tab and each embed page (**Embeds → Widget / Contact Form / Ticket Center**) show a dedicated **Helpdesk ID** box — a read-only field with the value and a **Copy Helpdesk ID** button. It's also the `data-helpdesk-id` value pre-filled in each embed snippet, and `get_embed_snippets` over MCP returns snippets with it filled in.

---

## Step 2 — Which surface should I use?

| Surface | What it is | Use it when |
|---|---|---|
| **Floating widget** | Site-wide bubble with search, AI answers, and messaging. Anonymous self-service on any public page. | You want help available everywhere with one script tag. |
| **Contact form** | A simple "email us" form that creates a support ticket. No login required. | You want a lightweight contact/support form on a page. |
| **Ticket center** | An authenticated area **inside the customer's logged-in account** where a known, signed-in user sees all their past tickets/conversations and starts new ones. Requires email + HMAC signature. | You have logged-in users and want them to manage their own support history. |
| **Hosted help center** | The public, SEO-indexed knowledge base. Lives at the Helpdesky address by default, or on the customer's own subdomain, in a subfolder of their site, or headless inside their own templates. | You want a docs/help site. Link to it, proxy it, or render it — see "Help center hosting". |
| **Content API** | REST API to create/update/delete categories and articles (or the same through MCP). | You want to programmatically write or keep help articles in sync. |
| **Inbound email** | Forward the customer's existing support inbox into Helpdesky as conversations. | They already have a support@ address. Dashboard + mail-provider setup, not code. |

You can combine these. A common setup is: floating widget on every page + help center linked from the nav + ticket center inside the account area.

---

## Dashboard setup (the customer clicks, or you do it over MCP)

Several features silently do nothing until they are configured in the Helpdesky dashboard. With MCP you can do everything in this table yourself except the rows marked **human only**. Without MCP, walk the customer through whichever apply and tell them exactly which menu to open.

| Setting | Where in the dashboard | Needed for | Required? |
|---|---|---|---|
| **AI provider API key** (OpenAI / Anthropic / Google / etc.) | **Settings → AI Settings** (MCP: `update_ai_settings`) | The widget's "Ask AI" tab, AI semantic search, and AI article generation. The customer brings their own key — there is no platform fallback. | Required for any AI feature |
| **Enable Widget** | **Embeds → Widget** (MCP: `update_helpdesk_settings` `widgetEnabled`) | The floating widget to appear at all | Required for the widget |
| **Messaging** | **Settings → General** "Messaging" switch, or the **Enable messaging** button on the **Messages** page while it is off (MCP: `messagingEnabled`) | Widget messaging, contact form & ticket center to actually send/receive messages | Required for contact form + ticket center |
| **Allowed Embed Domains** | **Settings → Messages** tab, section **Allowed Embed Domains** (MCP: `allowedDomains`) | Restricting which sites may embed the widget, contact form and ticket center; add the customer's site domain (subdomains included automatically; empty = any domain) | Recommended for embeds |
| **HMAC secret** | **Embeds → Ticket Center → HMAC Authentication** — **human only** | Generating the secret used to sign ticket-center users | Required for ticket center |
| **API key** (`hdh_`) | **Settings → API Integration** — **human only** | Calling the REST Content API directly (not needed over MCP) | Only for direct REST use |
| **Cloudflare Turnstile** | **Embeds → Contact Form** | Bot/spam protection — paste a Turnstile **Site Key + Secret Key**; the embed code doesn't change | Optional, strongly recommended for the contact form |
| **Helpdesk name** | **Settings → General** | Shown on the help center, widget and emails | Required |
| **URL slug & hosting** | **Help Center → Domain & Address** (MCP: hosting tools) | The help center address: Helpdesky address, your own subdomain, subfolder on your website, or headless — see "Help center hosting" | Required (defaults to the Helpdesky address) |
| **Branding** (logo, favicon, colors, theme, layout: Default / Sidebar / Docs) | **Branding** page | Look & feel of the help center and embeds | Optional |
| **Remove "Powered by Helpdesky"** | **Settings → General** | White-labeling public pages | Optional, plan-gated |
| **Publish articles** | **Articles** page (MCP: `create_article`, `publish_article`) | Anything to show in the help center and widget/AI search | Required for useful content |
| **Notifications** | **Settings → Notifications** | Choosing who gets emailed about new messages | Recommended with messaging |
| **Data Sources** | **Settings → Data Sources** | Extra URLs for the AI to learn from, improving AI answers | Optional |
| **Billing / plan** | **Billing** — **human only** | Plan limits (articles, white-label, more helpdesks) | As needed |

Key dependencies to call out to the customer:
- The widget's **AI answers won't work without an AI provider key** in **Settings → AI Settings** — the "Ask AI" tab simply stays hidden until a key is added.
- The **contact form and ticket center won't process messages unless messaging is enabled**, and (if any are listed) the site domain is in **Allowed Embed Domains**.
- Articles must be **published** to appear in the help center, widget search, or AI answers.

---

## Widget embed

One script tag, anywhere in the page (ideally before `</body>`). It injects a floating launcher with Shadow-DOM isolation, so it won't clash with the host app's styles.

```html
<script src="https://helpdesky.io/widget.js" data-helpdesk-id="YOUR_HELPDESK_ID"></script>
```

- `data-helpdesk-id` (required) — the helpdesk UUID.
- `data-auto-open` (optional) — add it to open the panel as soon as the widget loads (demo or onboarding pages). Omit it, or set `"false"`, for the normal closed launcher.
- Appearance (position, colors, tab order, AI on/off, messaging mode, etc.) is configured on **Embeds → Widget**, not via attributes.

The widget respects **Allowed Embed Domains** too: if the list is non-empty and the page's domain is not on it, the widget does not load.

---

## Contact form embed

A target `<div>` plus a script tag. The form renders inside the div (Shadow-DOM isolated).

```html
<div id="hdh-contact-form"></div>
<script src="https://helpdesky.io/contact-form.js"
  data-helpdesk-id="YOUR_HELPDESK_ID"></script>
```

Optional attributes:
- `data-align="center|left|right"` — alignment within its container (default `center`).
- `data-max-width="560px"` — max form width (default `560px`).
- `data-container="hdh-contact-form"` — custom target div id (must match the `<div>` id).

If **Allowed Embed Domains** is non-empty, the customer's domain must be on it, or the submit request is rejected with a CORS/"Origin not allowed" error.

The dashboard's copy snippet appends a cache-busting `?v=8` to the script URL — it's optional, and the plain `contact-form.js` URL works identically.

**Recommend Cloudflare Turnstile (spam protection).** By default the contact form has no bot protection, so it can attract spam. Advise the customer to create a free Turnstile widget in their Cloudflare dashboard and paste the **Site Key** and **Secret Key** into the **Cloudflare Turnstile** section of **Embeds → Contact Form**. This is a dashboard-only setting — **the embed code above does not change** and the form still works without it. The Secret Key is stored server-side; only the Site Key reaches the browser.

**Tip — blend into the page.** If the form looks like a separate card, turn on **Hide outer border** in **Embeds → Contact Form** to remove the outer border, background, and shadow so the embed blends into the surrounding page.

---

## Ticket center embed (authenticated)

The ticket center shows a signed-in user their own tickets. It authenticates the user with an **HMAC-SHA256 signature of their email**, computed **server-side** with the HMAC secret. The secret must never reach the browser.

### Server-side signing recipe

**Normalize the email to `email.trim().toLowerCase()` first, then use that exact same normalized string for BOTH the signature and the `data-email` attribute.** The server lowercases the email before verifying, and a case- or whitespace-mismatch between what you signed and what you put in `data-email` is the #1 cause of "Verification failed". Output is a hex digest.

Node.js:
```js
import crypto from "crypto";

// HELPDESKY_HMAC_SECRET is a server-only secret/env var
const email = userEmail.trim().toLowerCase();
const signature = crypto
  .createHmac("sha256", process.env.HELPDESKY_HMAC_SECRET)
  .update(email)
  .digest("hex");
// render the embed with this SAME `email` in data-email and `signature` in data-signature
```

Python:
```python
import hmac, hashlib, os

email = user_email.strip().lower()
signature = hmac.new(
    os.environ["HELPDESKY_HMAC_SECRET"].encode(),
    email.encode(),
    hashlib.sha256,
).hexdigest()
```

PHP:
```php
$email = strtolower(trim($userEmail));
$signature = hash_hmac('sha256', $email, getenv('HELPDESKY_HMAC_SECRET'));
```

Ruby and Shopify Liquid recipes are shown on **Embeds → Ticket Center** (and returned by `get_embed_snippets` over MCP, with the secret replaced by a placeholder).

### Embed markup

Render this only for logged-in users, injecting the **same normalized email** you signed (the `email.trim().toLowerCase()` value, not the raw input) and the server-computed signature:

```html
<div id="hdh-ticket-center"></div>
<script src="https://helpdesky.io/ticket-center.js"
  data-helpdesk-id="YOUR_HELPDESK_ID"
  data-email="USER_EMAIL"
  data-signature="HMAC_SIGNATURE"></script>
```

Optional: `data-align`, `data-max-width` (default `864px`), `data-container` (default `hdh-ticket-center`).

If **Allowed Embed Domains** is non-empty, the customer's domain must be on it for the ticket-center endpoints to respond. As with the contact form, the dashboard snippet appends an optional cache-busting `?v=8`; the plain `ticket-center.js` URL works the same.

**Tip — blend into the page.** A matching **Hide outer border** toggle lives in **Embeds → Ticket Center**; enable it to drop the outer border, background, and shadow so the embed blends into the account area instead of looking like a separate card.

### Styling the contact form and ticket center

Both embeds render inside a **Shadow DOM**: the host page's stylesheets do not reach them and they expose **no CSS custom properties** (no `--hdh-*` variables). Style them through their settings, never by targeting the host page.

- **Custom CSS keys.** `contactFormCss` (contact form) and `ticketCenterCss` (ticket center) hold plain CSS that is injected as a `<style>` inside the embed, unchanged — the same text as the **Custom CSS** textarea on **Embeds → Contact Form** / **Embeds → Ticket Center**. Most elements carry inline styles, so declarations usually need `!important`.
- **Structured styling keys.** Contact form: `cfAccentSource` (`gradient` | `primary`), `contactFormButtonColor`, `cfButtonTextColor`, `cfLabelColor`, `cfBgColor`, `cfUseBranding`, `cfHideBorder`, `contactFormAlign`, the text keys (`contactFormButtonText`, `contactFormSuccessHeading`, `contactFormSuccessBody`, `cfNameLabel` … `cfMessagePlaceholder`) and `customFields`. Ticket center: `ticketCenterStyle` (`default` | `polaris`), `tcAccentSource`, `tcHideBorder`, `tcShowEmailField`, the colour keys `tcColorPrimary`, `tcColorPrimaryHover`, `tcColorPrimaryText`, `tcColorBg`, `tcColorSurface`, `tcColorText`, `tcColorTextSubdued`, `tcColorBorder`, `tcColorAccent`, `tcColorBubble`, the radii `tcRadiusButton` / `tcRadiusCard` (px), the label keys `tcLabelTitle` … `tcLabelSubject` and `customFields`. Colours are 6-digit hex; an empty string restores the default.
- **Contact form selectors:** `.hdh-cf-form` (the form card), `.hdh-cf-form label`, `#hdh-cf-name`, `#hdh-cf-email`, `#hdh-cf-message`, `#hdh-cf-<fieldId>` (custom fields), `.hdh-cf-error`, `.hdh-cf-submit`, `.hdh-cf-success`. The Turnstile challenge (`#hdh-cf-turnstile-outer`) is rendered in the host page outside the shadow root, so style it from the page's own CSS.
- **Ticket center selectors:** `.hdh-tc-wrapper`, `.hdh-tc-header`, `.hdh-tc-title`, `.hdh-tc-btn` / `.hdh-tc-btn-primary` / `.hdh-tc-btn-send` / `.hdh-tc-btn-destructive` / `.hdh-tc-btn-back` / `.hdh-tc-btn-mark-resolved`, `.hdh-tc-badge` with `.hdh-tc-badge-pending` / `-unresolved` / `-resolved`, `.hdh-tc-list`, `.hdh-tc-item`, `.hdh-tc-item-subject`, `.hdh-tc-item-preview`, `.hdh-tc-item-time`, `.hdh-tc-empty`, `.hdh-tc-loading`, `.hdh-tc-error`, `.hdh-tc-messages`, `.hdh-tc-subject-input`, `#hdh-tc-cf-<fieldId>` (custom checkbox fields), `.hdh-tc-bubble` with `.hdh-tc-bubble-visitor` / `.hdh-tc-bubble-agent`, `.hdh-tc-composer`, `.hdh-tc-editor-box` (the bordered reply box), `.hdh-tc-toolbar`, `.hdh-tc-editor` (the reply editor), `.hdh-tc-editor-empty`.
- **Over MCP**, `get_contact_form_settings` and `get_ticket_center_settings` return the saved config plus a `reference` block with every key (type, allowed values, default, purpose), every selector with what it targets, and the dashboard's starter CSS snippets; write the CSS back with `update_contact_form_settings` / `update_ticket_center_settings`. Without MCP, paste the CSS into the **Custom CSS** textarea of the matching **Embeds** page.

---

## Framework placement notes

- **Plain HTML** — paste the script tag(s) before `</body>`. For the ticket center, compute the signature in a server-side template (PHP, etc.) and inject `data-email`/`data-signature`.
- **React / Vite (SPA)** — inject the widget/contact-form script once on mount, e.g. in a top-level component:
  ```jsx
  useEffect(() => {
    const s = document.createElement("script");
    s.src = "https://helpdesky.io/widget.js";
    s.setAttribute("data-helpdesk-id", import.meta.env.VITE_HELPDESK_ID);
    document.body.appendChild(s);
    return () => { s.remove(); };
  }, []);
  ```
  For the **ticket center**, the HMAC signature must come from your backend (an API route that signs the logged-in user's email); the SPA fetches `{ email, signature }` and then injects the ticket-center script with those attributes. Never put the HMAC secret in `VITE_`-prefixed env vars — those ship to the browser.
  - **SPA mount order (contact form & ticket center):** these two scripts look up their target `<div>` once, the instant they run, and silently do nothing if it's missing (no retry/observer). So the `<div id="hdh-contact-form">` / `<div id="hdh-ticket-center">` must already be in the DOM **before** the script executes. Render the `<div>` first, then inject the script from a post-mount `useEffect` — don't place a literal `<script>` tag in JSX (React won't execute those). The widget needs no container div, so it can be injected anywhere.
- **Next.js** — put the widget/contact-form tag in a Client Component or `next/script`. For the ticket center, sign in a Server Component / Route Handler / `getServerSideProps` using a server-only env var (no `NEXT_PUBLIC_` prefix), then pass `email` + `signature` into the rendered script tag.

---

## Help center hosting (Help Center → Domain & Address)

The customer's help center is **already live and hosted by Helpdesky**; the **Domain & Address** page decides where visitors find it. Pick one option with the customer (they can switch later). The page shows the live address at the top; only the modes below exist — there is no `{slug}.helpdesky.io` subdomain.

| Mode (card title) | Address | What it takes |
|---|---|---|
| **Helpdesky address** | `https://helpdesky.io/help/{slug}` | Nothing. The default; keeps working alongside every other mode. |
| **Your own subdomain** | `https://help.yourdomain.com` | One CNAME record; Helpdesky issues the certificate. |
| **Subfolder on your website** | `https://yourdomain.com/help` | A reverse proxy rule in the customer's site — code you write. Best for SEO. |
| **Headless (your own templates)** | `https://yourdomain.com/help` rendered by the customer's own code | Three routes in the customer's site fetching the public Content API — code you write. |

Links in the app ("Help" / "Docs" in the nav or footer, "Learn more" next to features) must point at the **live** address, which you read from the top of Domain & Address or from `get_hosting_status` over MCP. Configured is not the same as active: a saved subfolder or headless address becomes the live one only after **Check setup** passes (and, for headless, the customer confirms **Make it live**). Until then the Helpdesky address (or the connected subdomain) stays live.

### Deep links (every mode)

The path shape is the same everywhere; only the base changes:

- Article: `{base}/{article-slug}`
- Category: `{base}/category/{category-slug}`

where `{base}` is `https://helpdesky.io/help/{slug}`, `https://help.yourdomain.com`, `https://yourdomain.com/help`, or the saved headless address. The `{article-slug}` is auto-generated from the title and visible in the dashboard, returned by the Content API, and returned by the MCP article tools.

### Your own subdomain

1. The customer enters a **subdomain** they own (e.g. `help.yourdomain.com`) under **Your own subdomain** and clicks **Connect domain** (MCP: `set_custom_domain`). Root/apex domains (`yourdomain.com`) are not accepted here — use the subfolder option for that.
2. At their DNS provider they add a **CNAME** for that subdomain pointing to **`origin.helpdesky.io`**. On Cloudflare the record must be **"DNS only"** (grey cloud), not "Proxied" (orange cloud), or the certificate cannot be issued.
3. The **Setup progress** checklist on the page (DNS record → domain verified → certificate issued → live) refreshes on its own, usually within minutes. If verification stalls, the page shows an extra TXT record to add.

You cannot change the customer's DNS; tell them the exact record. Not available on site builders without DNS access either way.

### Subfolder on your website (reverse proxy — you write this)

Best for search rankings: the help content lives on the customer's own domain. Not possible on site builders such as Wix, Squarespace, Shopify or WordPress.com (use the subdomain instead).

1. On **Domain & Address → Subfolder on your website**, save the **Public address** (e.g. `https://yourdomain.com/help`) with **Save address** (MCP: `save_subfolder_hosting`).
2. Add a forwarding rule to the customer's site that proxies everything under the prefix to **`https://helpdesky.io/help/{slug}`**, strips the prefix, and sends the visitor's host in **`X-Forwarded-Host`**. It must be a proxy/rewrite (the browser URL stays on the customer's domain), never a redirect. Only the prefix needs forwarding — images, search and Ask AI load from Helpdesky directly. The dashboard shows ready-made snippets for **Cloudflare Workers, Vercel (`vercel.json` rewrites), Next.js (`next.config.js` rewrites), Netlify (`netlify.toml` 200 redirects with an `X-Forwarded-Host` header) and nginx (`location` blocks with `proxy_pass`, `Host helpdesky.io`, `X-Forwarded-Host $host`)**, filled in with the real domain and slug; `get_hosting_instructions` over MCP returns the same. Use those rather than writing a proxy from memory. Vercel and Next.js external rewrites forward the host automatically; on Cloudflare the Worker sets it; on Netlify and nginx the header is explicit in the snippet.
3. Add `Sitemap: https://yourdomain.com/help/sitemap.xml` to the site's own `robots.txt` so search engines find the help center sitemap.
4. Deploy, then click **Check setup** (MCP: `check_subfolder_setup`). It loads the public address through the customer's site and reports five steps: address answers, requests reach Helpdesky, prefix removed, `X-Forwarded-Host` arrives, a help center page loads. Fix whichever step fails, redeploy, check again.
5. When the check passes, the customer confirms and the subfolder address becomes the live one (MCP: `activate_subfolder_hosting`). Dashboard links, the widget and canonical URLs switch to it; the Helpdesky address keeps serving and points search engines at the new one.

If the customer later changes the help center slug, the proxy target changes with it — update the rule.

### Headless (your own templates — you write this)

The customer's site fetches articles from the public **Content API** and renders them in its own templates, owning layout, navigation, search, sitemap and 404s. **Fetch and follow the self-contained spec at https://helpdesky.io/docs/headless.md** — it defines the endpoints (`/api/content/v1/helpdesk/{slug}/…`), the stylesheet, the three routes (`base/`, `base/<article-slug>`, `base/category/<category-slug>`), SEO/canonical rules, the redirect rule and the acceptance criteria. Do not reconstruct it from this file.

Flow on **Domain & Address → Headless (your own templates)**: save the **Public address** (MCP: `save_headless_hosting`), build and deploy the routes, publish at least one article, click **Check setup** (MCP: `check_headless_setup`; it opens one published article on the customer's site and looks for its title), then confirm **Make it live** (MCP: `activate_headless_hosting`). Headless and subfolder cannot be configured at the same time — remove one before saving the other.

---

## Auto-write & sync help articles

Over MCP, use `create_article`, `publish_article` and the category/image tools — no API key involved. For code that keeps docs in sync on its own (CI, a build step, your app's backend), use the REST Content API below. Authenticate every request with the `X-API-Key` header (keep the key server-side).

- **Base URL:** `https://helpdesky.io/api/v1`
- **Auth header:** `X-API-Key: hdh_your_api_key`
- **Content type:** `application/json` (except image upload, which is multipart)

### Categories
- `GET /categories` — list.
- `POST /categories` — body: `name` (required); optional `description`, `icon`. Slug auto-generated. (`order` can only be set later via `PATCH`.)
- `PATCH /categories/:id` — update `name` / `description` / `icon` / `order`.
- `DELETE /categories/:id` — remove.

### Articles
- `GET /articles` (optional `?categoryId=`) — list.
- `POST /articles` — body: `title` (required), `content` (required, **Markdown**); optional `excerpt`, `categoryId`, `slug`, `published`, `numberHeadings`, `hiddenFromWidget`.
- `PATCH /articles/:id` — update any of the above plus `order` (position within its category).
- `DELETE /articles/:id` — remove.

### Images
- `POST /images` — multipart form with an `image` field (≤ 5 MB; **JPG / PNG / GIF / WebP** only — SVG is rejected). Returns `{ url, filename, size, contentType }`.
- Embed the returned `url` in article content. **Prefer an HTML `<img>` tag so you can size the image** — this is the same format the dashboard editor emits:
  ```html
  <img src="/api/images/uploads/.../screenshot.png" width="400" />
  ```
  Markdown `![alt](url)` also works but always renders at the image's full original size (no width/height control). Both formats can be mixed in the same article.

### Rich article formatting (Markdown extras)
Article `content` is Markdown. Beyond standard syntax (headings, **bold**, *italic*, lists, links, `code`, fenced code blocks, blockquotes, and GitHub-style tables), Helpdesky renders these extras on the help center and widget — use them so generated docs look polished instead of plain-text:

- **Callouts** — a **blockquote** whose first line is `[!info]`, `[!warning]`, or `[!danger]` (the `>` markers are required — a plain paragraph won't render as a callout):
  ```
  > [!warning]
  > Back up your data before continuing.
  ```
- **Buttons** — a link with a `{.btn}` (filled) or `{.btn-outline}` modifier; optionally add `.nofollow` and/or `.same-tab` (open in the same tab). Consecutive button lines render side by side as one row:
  ```
  [Get started](https://app.example.com/signup){.btn}
  [Read the guide](/help/acme/setup){.btn-outline .same-tab}
  ```
- **Action cards** — a link with `{.card ...}`; optional `icon="…"`, `desc="…"`, `sameTab`, `nofollow`. Two or more consecutive cards form a grid:
  ```
  [Quick start](/help/acme/quick-start){.card icon="rocket" desc="Up and running in 5 minutes"}
  [Watch a demo](/help/acme/demo){.card icon="play" desc="2-minute video tour"}
  ```
  Valid `icon` values: `user-plus`, `play`, `book-open`, `rocket`, `settings`, `mail`, `download`, `link`, `info`, `check`, `sparkles`, `code` (an unknown name falls back to `info`).
- **YouTube embeds** — put a YouTube link on its own line (or as a plain Markdown link); `youtube.com/watch?v=…`, `youtu.be/…`, and `youtube.com/embed/…` auto-embed as a responsive player.

### Example: create a category + published article

```js
const BASE = "https://helpdesky.io/api/v1";
const headers = {
  "X-API-Key": process.env.HELPDESKY_API_KEY, // server-side secret
  "Content-Type": "application/json",
};

// 1. Category
const cat = await fetch(`${BASE}/categories`, {
  method: "POST", headers,
  body: JSON.stringify({ name: "Getting Started", description: "Set-up help" }),
}).then(r => r.json());

// 2. Article (Markdown content), published so it appears
const article = await fetch(`${BASE}/articles`, {
  method: "POST", headers,
  body: JSON.stringify({
    title: "How to create your first project",
    content: "## Welcome\n\n1. Click **New Project**\n2. Name it\n3. Done!",
    categoryId: cat.data.id,
    published: true,
  }),
}).then(r => r.json());

console.log("Created:", article.data.title, "→", article.data.slug);
```

### Keeping docs in sync
To revise docs as the app evolves, store each article's returned `id` and call `PATCH /articles/:id` to update `content`/`title`, or `DELETE /articles/:id` to remove. Same pattern for categories.

### Notes
- Articles must be **`published: true`** to appear in the help center and widget.
- **Slugs auto-generate** from the title (or pass an explicit `slug`); changing a published article's slug creates a redirect from the old one.
- **Plan-based article limits** are enforced — `POST /articles` returns HTTP 403 with code `ARTICLE_LIMIT_REACHED` when the limit is hit; surface that to the customer (they need to upgrade).
- **Rate limit: 60 requests/minute per API key.** Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` headers; exceeding it returns HTTP 429 `{ "error": "Rate limit exceeded. Please try again later." }`. When seeding or syncing many articles, pace requests to stay under 60/min (watch `RateLimit-Remaining`) and back off / retry after `RateLimit-Reset` on a 429.
- Authoritative, always-current reference lives at **https://helpdesky.io/docs/api**.

---

## Inbound email support channel (dashboard setup, not code)

The customer can turn their existing support inbox into Helpdesky conversations by forwarding mail to:

```
{slug}-{code}@inbox.helpdesky.io
```

The 6-digit `{code}` is shown on the **Embeds → Email Forwarding** dashboard page and can be regenerated there. Forwarded emails become conversations in the inbox.

This is configured in the customer's email provider (e.g. a forwarding rule on their support@ address) — **you cannot perform the forwarding setup**, with or without MCP. Walk the customer through it and point them to the **Embeds → Email Forwarding** page for the exact address and code.

---

## Troubleshooting

- **Ticket center "Verification failed" / signature rejected.** Almost always an email-normalization mismatch: the value you signed must equal `data-email` after the server lowercases it. Sign and send the same `email.trim().toLowerCase()` value. To debug, use the **Verification Tool** under **HMAC Authentication** on **Embeds → Ticket Center**: paste an email, generate a test signature, and compare it to what your server produces — enter the email already trimmed + lowercased so it matches your server-side normalization.
- **Contact form / ticket center renders nothing in an SPA.** The script looks up its `<div>` once and bails silently if it's missing. Ensure the `<div id="hdh-…">` exists before the script runs (render the div first, inject the script in a post-mount `useEffect`).
- **Widget, contact form or ticket center does not load, or the request is rejected (CORS / "Origin not allowed").** The site's domain isn't in a non-empty **Allowed Embed Domains** list (**Settings → Messages**), or messaging isn't enabled — turn it on under **Settings → General** or with **Enable messaging** on the **Messages** page.
- **Widget shows but has no "Ask AI" tab.** No AI provider key is configured (**Settings → AI Settings**) — the AI tab stays hidden until the customer adds their own key.
- **Subfolder "Check setup" fails.** Read the failing step: a redirect instead of a proxy, the prefix not stripped, a missing/mismatched `X-Forwarded-Host`, or the rule pointing at the custom domain instead of `helpdesky.io/help/{slug}`. The dashboard snippets already handle all of these.
- **Headless "Check setup" fails.** Publish at least one article first; the article route must return HTTP 200 with the article title in the page. Re-read https://helpdesky.io/docs/headless.md.
- **Content API returns 429.** You exceeded 60 requests/minute — slow down and retry after `RateLimit-Reset`.
- **MCP tool missing.** Hosting, embeds, settings and AI tools are in the **Setup** tool set; enable it with `enable_tool_sets` or on the customer's MCP page.

## Verification checklist before you finish

- Helpdesk ID / API key / HMAC secret are stored as secrets, not hardcoded, and the HMAC secret + API key never reach the browser.
- The required dashboard setup is done for the surfaces you added — by you over MCP, or by the customer (**Enable Widget**, messaging on, **AI provider key** if AI answers are expected).
- The customer's domain is in **Allowed Embed Domains** if that list is non-empty and you embedded the widget, contact form or ticket center.
- Ticket-center signatures are computed server-side over the **trimmed + lowercased** email, and `data-email` carries that same normalized value.
- Help-center and deep-link URLs use the **live** address from **Domain & Address** (`helpdesky.io/help/{slug}` unless a subdomain, subfolder or headless address is active).
- If you set up a subfolder or headless address, **Check setup** passed and the address was made live; otherwise links still point at the previous address on purpose.
- Articles you create are `published: true` if they should be visible.
