# hlpd > hlpd builds an in-app help centre from source, with walkthroughs, support chat and bug reports for iOS, Android and web. A coding agent can sign up and create the first app with one API call. # Getting started Source: https://hlpd.uk/docs/ hlpd has four parts: the admin where you manage apps and tickets, the generator that builds a help bundle from your source, an SDK for each platform, and the API those SDKs talk to. > Using a coding agent? Start with [For coding agents](https://hlpd.uk/docs/agents) to create an account and app with one API call. ## Create an app [Create an account](https://api.hlpd.uk/admin/signup), then add an app under **Apps** in the admin. Each app has two identifiers: | Identifier | Looks like | Used for | |---|---|---| | Public project id | `brp_…` | Bug reports, feature requests, ticket threads, the hosted report form and the web widget | | SDK key | `hk_…` | The help centre, chat and analytics, sent as `Authorization: Bearer` on every `/v1/*` request | SDK keys ship inside your app, so treat them as public; the app's quotas limit what a key can spend. For a website, list its origins on the app so reports from other origins are refused. ## Generate a help bundle The [generator](https://hlpd.uk/docs/generator) reads your app's source, writes help topics, records Show me and Guide me walkthroughs with narration and builds a local search index. Everything the SDK needs offline goes into one directory: the help bundle. ```sh .venv/bin/python -m hlpd_generator generate ~/Code/drink_ios --platform ios --out /tmp/hlpd/drink ``` The bundle is written to `/tmp/hlpd/drink/bundle`. Ship it inside your app, and publish it to the app so installed copies can update without a store release: ```sh HLPD_EMAIL=me@example.com HLPD_PASSWORD=... \ .venv/bin/python -m hlpd_generator publish /tmp/hlpd/drink/bundle --project brp_... ``` ## Add the SDK - [iOS](https://hlpd.uk/docs/ios): Swift package with SwiftUI views. iOS 17. - [Android](https://hlpd.uk/docs/android): Compose library module. minSdk 28. - [Web](https://hlpd.uk/docs/web): ES modules with no build step. Each SDK takes the SDK key, the bundle and, optionally, the public project id for tickets. Without a project id, the chat hands escalations to your own code instead of opening a ticket. ## Triage and learn - **Reports** and **Reader** in the admin list every bug report and feature request, with screenshots, device details and, for reports sent through the SDK, the help topics the user had already viewed. Reply in the ticket thread; with reply notifications set up, the reporter gets a push. - **Insights** on each app shows which topics people open, what they search for and ask, and the gaps: questions with no good answer, and reports filed after a visit to the help. - Pass those gaps back to the generator with `--gaps queries.json` so the next bundle answers them. - The native admin app for iPhone, iPad and Mac shows the same inbox, with push alerts for new reports. ## Identifiers in requests | Name | Where it appears | |---|---| | Device id | 16 base62 characters, generated once per install. Sent as `X-Hlpd-Device` on `/v1/*` and as `client_id` on reports, so a device's reports and help activity are linked. | | Bundle id | `bundleId` in the bundle's `manifest.json`, sent as `X-Hlpd-Bundle` with `X-Hlpd-Bundle-Version`. Ordering and analytics are keyed on it. | # For coding agents Source: https://hlpd.uk/docs/agents Create an account and first app with one API call, then add in-app help and support using the returned `hk_…` SDK key and `brp_…` project id. ## What an agent gets in one call One request creates an owner account on the Free plan and the first app. No card or browser session is needed. hlpd sends a verification email to the address; the account works straight away, and only upgrading to a paid plan needs a verified email address. | Response field | Use | |---|---| | `token` | Owner bearer token for the admin API and generator. Keep it secret. | | `owner` | The account's `email`, `plan` (`free`) and `email_verified`. | | `verification` | `sent` once the verification email has gone to `owner.email`. | | `project.id` | Public project id, `brp_…`, for reports and the hosted report form. | | `project.sdk_key` | SDK key, `hk_…`, for help, chat and analytics. It ships in the app and is public. | | `project.report_url` | Hosted report form for this app. | | `project.api_url` | API base URL for the SDK. | | `docs` | Link to this guide. | The signup token is the same kind of owner bearer token that `POST /admin/api/session` returns. Keep the owner token and password out of git and out of the app. [GET https://api.hlpd.uk/api/agent](https://api.hlpd.uk/api/agent) returns the agent quickstart as Markdown. The site also provides [llms.txt](https://hlpd.uk/llms.txt), [llms-full.txt](https://hlpd.uk/llms-full.txt) and a [downloadable skill file](https://hlpd.uk/docs/hlpd-skill.md). ## Sign up Use the human's email address and generate a long random password, then send JSON to `POST https://api.hlpd.uk/api/signup`: ```sh curl -X POST https://api.hlpd.uk/api/signup \ -H 'Content-Type: application/json' \ -d '{"email": "...", "password": "...", "project": "My app"}' ``` The response is `201`: ```json { "token": "...", "owner": {"email": "...", "plan": "free", "email_verified": false}, "verification": "sent", "project": { "id": "brp_…", "name": "My app", "sdk_key": "hk_…", "report_url": "https://api.hlpd.uk/report/brp_…", "api_url": "https://api.hlpd.uk" }, "docs": "https://hlpd.uk/docs/agents" } ``` Errors use `{"error": {"code": "...", "message": "..."}}`: | Code | HTTP status | What to do | |---|---|---| | `email_taken` | 409 | The email address already has an account. Ask the human to [sign in](https://api.hlpd.uk/admin/login) or reset the password with `POST /api/password/forgot`. | | `invalid_request` | 422 | Fix the body; `message` says what is wrong. | | `rate_limited` | 429 | There are 5 signups per IP per hour. Wait or ask the human to [sign up](https://api.hlpd.uk/admin/signup). | | `signup_disabled` | 403 | Ask the human to [sign up](https://api.hlpd.uk/admin/signup). | Keep the token in an environment variable or secret store, never in git. Tell the human the email address and password at the end, and ask them to open the verification email. They can [sign in](https://api.hlpd.uk/admin/login) to manage the app and tickets. A forgotten password is reset by email; see [Accounts and email](https://hlpd.uk/docs/server#accounts-and-email). ## Put the key and project id in the app Use `project.sdk_key` for `hk_…` and `project.id` for `brp_…`. SDK keys and project ids are public; owner tokens and passwords are secrets. ### iOS Add `sdk/ios/Hlpd` as a Swift package dependency and the generated bundle as a folder reference named `HlpdBundle`. ```swift import Hlpd let hlpd = HlpdConfiguration( apiKey: "hk_…", // the project's SDK key tickets: HlpdTickets(serverURL: URL(string: "https://api.hlpd.uk")!, projectID: "brp_…"), // the project's public id context: { ["screen_name": "Settings"] } ) ``` See the [iOS guide](https://hlpd.uk/docs/ios) for help and support views. ### Android Include `:hlpd-sdk` as a library module, add `implementation(project(":hlpd-sdk"))`, copy the bundle to `src/main/assets/hlpd_bundle/` and set `androidResources { noCompress += "bin" }` in the host application module. ```kotlin import uk.hlpd.sdk.* val configuration = HlpdConfiguration( apiKey = "hk_…", bundle = HlpdBundleSource.Assets("hlpd_bundle"), tickets = HlpdTickets( serverUrl = "https://api.hlpd.uk", projectId = "brp_…", ), onEscalate = { ticketId, summary -> /* Observe a sent ticket or handle app-owned support. */ }, context = { mapOf("screen_name" to "Settings") }, ) // Inside Compose: HelpCenter(configuration) HelpSupport(configuration) HelpSuggestions(text = reportDescription, configuration = configuration, surface = "bug_report") ``` See the [Android guide](https://hlpd.uk/docs/android). ### Web Serve the SDK's `src/` directory and the bundle as static files, and allow your site's origin on the hlpd app; there is no npm install or build step. ```js import { Hlpd } from "./hlpd/src/index.js"; const hlpd = await Hlpd.load({ apiKey: "hk_…", bundleURL: "/hlpd-bundle/", // baseURL defaults to https://api.hlpd.uk tickets: { serverURL: "https://api.hlpd.uk", projectID: "brp_…" }, context: () => ({ screen_name: "Settings" }), onEscalate: (ticketId, summary) => { /* Observe a sent ticket or handle app-owned support. */ }, }); const help = hlpd.mountHelpCenter(document.querySelector("#help")); const support = hlpd.mountSupport(document.querySelector("#support")); // help.unmount(); support.unmount(); hlpd.destroy(); ``` See the [web guide](https://hlpd.uk/docs/web) for mounting help, support and the report widget. ## Generate and publish a help bundle Set up the [generator](https://hlpd.uk/docs/generator), then run these commands. Use `--platform ios`, `--platform android` or `--platform web` for your app, and replace the source path, output path, token and project id. ```sh python -m hlpd_generator generate /path/to/app --platform ios --out /tmp/hlpd/my-app HLPD_TOKEN=... python -m hlpd_generator publish /tmp/hlpd/my-app/bundle --project brp_… ``` Authentication uses the owner bearer token in `HLPD_TOKEN`, or `HLPD_EMAIL` and `HLPD_PASSWORD`. Ship `/tmp/hlpd/my-app/bundle` inside the app too; publishing lets installed apps fetch updates. See the [generator guide](https://hlpd.uk/docs/generator) for setup and bundle layout. The raw upload endpoint is `POST /admin/api/projects/{id}/bundles` with a gzipped tar and the owner bearer token. ## File a bug report from the app - iOS: create `BugReporter` with the API URL and project id, then add `.bugReportSupportSheet(isPresented:reporter:)` to your support button. - Android: create `BugReporter` with the API URL, project id and application context, then show `BugReportSupportScreen(reporter = ..., onClose = ...)`. - Web: add the report widget or call `POST /api/reports`. See the [web guide](https://hlpd.uk/docs/web#report-widget). Use the returned `project.report_url` to open the hosted report form at `https://api.hlpd.uk/report/brp_…`. You can test report intake without a device: ```sh curl https://api.hlpd.uk/api/reports \ -F project_id=brp_… \ -F title="Reminders won't stop" \ -F kind=bug \ -F body="They keep buzzing after 10pm." \ -F client_id=7fQ2kLp9Xc3mVb8N \ -F 'metadata={"screen_name": "Settings"}' ``` Replace `brp_…` with your project id. Confirm the response is `201` and its JSON has a `ticket_id`. See the [server guide](https://hlpd.uk/docs/server#reports-and-tickets) for report fields and ticket threads. ## Add another app Use the owner bearer token to create another project: ```sh curl -X POST https://api.hlpd.uk/admin/api/projects \ -H "Authorization: Bearer $HLPD_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name": "Another app"}' ``` The `201` response contains `{"project": {...}}`. Save `project.id` and the first SDK key, `project.keys[0]`. The Free plan allows 1 app; creating an app beyond your plan's limit returns `403`. Sign in again to get an owner bearer token: ```sh curl -X POST https://api.hlpd.uk/admin/api/session \ -H 'Content-Type: application/json' \ -d '{"email": "...", "password": "..."}' ``` Save the returned `token` in `HLPD_TOKEN` or a secret store. ## Agent checklist 1. Sign up with `POST https://api.hlpd.uk/api/signup` using the human's email address and a long random password. 2. Save the owner token, project id and SDK key in a secret store or environment variables. The project id and SDK key are public and can ship in the app; never commit the owner token or password or put them in the app. 3. Install the SDK for the app's platform. 4. Initialise it with the SDK key and the public project id for tickets. 5. Generate a help bundle from the app's source and ship the bundle inside the app. 6. Publish the bundle with `python -m hlpd_generator publish` and the owner token in `HLPD_TOKEN`. 7. Add the platform's help or support entry point so users can file bug reports. 8. Send a test bug report with curl and confirm the `201` JSON response has a `ticket_id`. 9. Tell the human the email address, password and where to sign in: https://api.hlpd.uk/admin/login. Ask them to open the verification email; a paid plan needs a verified email address. ## Downloads Download the [hlpd skill](https://hlpd.uk/docs/hlpd-skill.md) and put it at `.claude/skills/hlpd/SKILL.md` for Claude Code, or copy its instructions into your agent's instructions file. [llms.txt](https://hlpd.uk/llms.txt) links to the guides; [llms-full.txt](https://hlpd.uk/llms-full.txt) contains all docs as Markdown. # iOS SDK Source: https://hlpd.uk/docs/ios The `Hlpd` Swift package adds the help centre, walkthroughs, support chat and ticket screens to a SwiftUI app. ## Requirements - iOS 17. The portable core also builds for macOS 14. - A help bundle from the [generator](https://hlpd.uk/docs/generator), and an SDK key and public project id from the admin. ## Install 1. Add `sdk/ios/Hlpd` from the hlpd repository to your project as a Swift package dependency. 2. Add the generated bundle directory to the app target as a folder reference named `HlpdBundle`. ## Configure ```swift import Hlpd let hlpd = HlpdConfiguration( apiKey: "hk_...", // the project's SDK key tickets: HlpdTickets(serverURL: URL(string: "https://api.hlpd.uk")!, projectID: "brp_..."), // the project's public id context: { ["screen_name": "Settings"] } ) ``` `baseURL` defaults to `https://api.hlpd.uk`. `context` returns the current screen name and any app state that helps answer a question. It is added to the chat prompt for each question and attached to ticket metadata. ## Show help and support ```swift HelpCenterView(configuration: hlpd) // help centre, search, chat, contact support HelpSupportView(configuration: hlpd) // ticket inbox, report composer, threads HelpSuggestionsView(text: $reportText, configuration: hlpd) // help shown above your own report form ``` The help centre searches on the device with BM25 plus a static embedding model, and offers **Show me** and **Guide me** for topics that have a walkthrough. ## Escalation When the chat cannot answer from your help topics, it offers to send the question to support. The user can edit the ticket title and summary before anything is sent, and the ticket then appears in the chat as a card that opens its thread. If you leave out `tickets`, escalations go to `onEscalate` with no ticket id and the chat says support is handled by the app, so your own support flow can take over. ## Suggestions above your own form `HelpSuggestionsView(text:configuration:)` takes the text the user is typing in your bug report or contact form. Once the text has three content words it waits 400 ms, then shows up to three "This might help" cards that clear the bundle's suggestion thresholds. Opening a card shows the topic and its walkthroughs without losing the draft. ## Device id The SDK generates one 16-character base62 device id per install and keeps it in `UserDefaults` under `Hlpd.deviceID` (`HlpdDeviceIDStore`). Help requests send it as `X-Hlpd-Device` and bug reports send it as `client_id`, so the server links a device's reports and help activity. ## Bug reports without the help centre `BugReporter` submits reports and feature requests to `POST /api/reports`, tracks the user's tickets and replies, and registers APNs tokens for reply pushes. `BugReportSupportView`, or the `.bugReportSupportSheet(isPresented:reporter:)` modifier, is a ready-made report list, composer and thread screen: ```swift let reporter = BugReporter(serverURL: URL(string: "https://api.hlpd.uk")!, projectID: "brp_...") Button("Support") { showsSupport = true } .bugReportSupportSheet(isPresented: $showsSupport, reporter: reporter) ``` For a custom interface, `prepare(title:body:kind:...)` returns a pending report whose `reportDetails` and `metadataDetails` can be shown to the user before `approved()` and `submit(_:)`. # Android SDK Source: https://hlpd.uk/docs/android The `hlpd-sdk` library draws the help centre, walkthroughs, support chat and ticket screens in Jetpack Compose. ## Requirements - minSdk 28. The SDK uses Kotlin 2.0.21 and Compose BOM 2024.09.00; its build uses Gradle 8.13, AGP 8.13.2 and compile and target SDK 36. - A help bundle from the [generator](https://hlpd.uk/docs/generator), and an SDK key and public project id from the admin. ## Install Include `:hlpd-sdk` as an Android library module and depend on it from the host app: ```kotlin dependencies { implementation(project(":hlpd-sdk")) } ``` Copy the generated bundle into the host's `src/main/assets/hlpd_bundle/`, and keep embedding binaries uncompressed in the **host application module**: ```kotlin android { androidResources { noCompress += "bin" } } ``` To read a bundle from the filesystem instead, use `HlpdBundleSource.Directory(File(path))`. ## Configure and show ```kotlin import uk.hlpd.sdk.* val configuration = HlpdConfiguration( apiKey = "hk_your_product_key", bundle = HlpdBundleSource.Assets("hlpd_bundle"), tickets = HlpdTickets( serverUrl = "https://api.hlpd.uk", projectId = "brp_your_project", ), onEscalate = { ticketId, summary -> /* Observe a sent ticket or handle app-owned support. */ }, context = { mapOf("screen_name" to "Settings") }, ) // Inside Compose: HelpCenter(configuration) HelpSupport(configuration) HelpSuggestions(text = reportDescription, configuration = configuration, surface = "bug_report") ``` `baseUrl` defaults to `https://api.hlpd.uk`. Tickets are optional: without them, escalation calls `onEscalate(null, summary)` and the chat shows "Support is handled by the app." With tickets, the user reviews and edits the title and summary before Send submits anything. Keyword search works as soon as the bundle loads, and the embedding model loads on `Dispatchers.IO`. Suggestions wait 400 ms, need three content words and show at most three cards. Opening a suggestion or walkthrough keeps the host's draft. ## Record your own report submissions If you keep your own report form with `HelpSuggestions` above it, call this after **your** submission succeeds: ```kotlin Hlpd.reportFiled( context = applicationContext, configuration = configuration, text = submittedDescription, ticketId = response.ticketId, surface = "bug_report", ) ``` It records the suggested topics in the order they were shown, the opened topics, the original description and the ticket id, then resets that surface's history. Reports and chat escalations sent by the SDK already record this. ## Runtime, events and updates `HlpdRuntime.shared(context, configuration)` exposes the shared bundle flow, search, client, events, chat and ticket service. Every event records the current screen. Events are sent in batches of at most 200: every 30 seconds in the foreground or at 20 events, and once more when the app goes to the background. The runtime checks `/v1/bundle` once per process. A newer published bundle downloads only the changed files, each verified against its SHA-256 and size, and becomes active on the next launch. Missing files or a stale manifest fall back to the bundle embedded in the app. Pins returned by `/v1/order` appear as amber **Service notice** cards above categories and search results. Expired notices disappear locally. ## Device id The SDK generates one 16-character base62 device id per install and stores it in the `Hlpd.identity` preferences; `HlpdIdentity.deviceId(storage)` reads it. Help requests send it as `X-Hlpd-Device` and bug reports send it as `client_id`. ## Bug reports without the help centre `uk.hlpd.sdk.bugreports.BugReporter` submits reports and feature requests to `POST /api/reports`, tracks the user's tickets and replies, and registers FCM tokens for reply pushes. Use the Compose `BugReportSupportScreen`, or `BugReportSupportActivity.intent(context, serverUrl, projectId, ...)` from a View-based app: ```kotlin val reporter = BugReporter("https://api.hlpd.uk", "brp_your_project", applicationContext) BugReportSupportScreen(reporter = reporter, onClose = { showSupport = false }) ``` ## Walkthroughs in your app Show me and Guide me need a host that knows your screens. Configure `walkthroughHost`, keep it stable with `remember`, and attach the controller around your app root: ```kotlin val liveRegistry = WalkthroughRegistry() val host = object : WalkthroughHost { override fun supports(script: WalkthroughScript, mode: WalkthroughMode) = script.topicId == "log-a-drink" // Validate every supported action and selector. override suspend fun begin(script: WalkthroughScript, mode: WalkthroughMode): WalkthroughSession = if (mode == WalkthroughMode.SHOW_ME) freshIsolatedSampleSession() else { navigateToLogAndDismissHelp(); liveLogSession(liveRegistry) } } val configuration = HlpdConfiguration(apiKey = "hk_key", walkthroughHost = host) val runtime = HlpdRuntime.shared(context, configuration) HlpdWalkthroughHost(runtime.walkthrough) { CompositionLocalProvider(LocalWalkthroughRegistry provides liveRegistry) { Button(onClick = { count++ liveRegistry.onActionCompleted("record-drink", "tap") // After the mutation commits. }, modifier = Modifier.hlpdWalkthroughTarget( "record-drink", label = "I had a drink", type = "button", actions = mapOf("tap" to { count++ }), )) { Text("I had a drink") } } } ``` - A host must explicitly support each script and mode. Show me must return a fresh session with `isIsolated = true` and a scene backed only by sample state; the SDK rejects a live session for Show me before any action. - Guide me returns a session for the real scene and navigates there, dismissing Help first. It never invokes callbacks: it accepts only matching committed events while a step is armed. - Show me supports Pause, Resume, Next, Replay and Close. Captions stay visible, with optional device read-aloud. `WalkthroughScript` and `WalkthroughStep` are in `uk.hlpd.sdk.core`. The demo's `DemoDrinkSession` is a complete scene and host example. ## Markdown and links Topic text is rendered as inert Compose text. Only HTTP, HTTPS, mailto and `hlpd://topic/` links are interactive, and images are read only from the bundle. No WebView, HTML execution or model runtime is used. # Web SDK Source: https://hlpd.uk/docs/web Dependency-free ES modules for the help centre, walkthroughs, support chat and a report widget. There is no npm install or build step. ## Requirements A current browser with ES modules, shadow DOM, native dialogs and `Intl.Segmenter`. Serve the SDK's `src/` directory and your generated bundle as static files, and allow your site's origin on the hlpd app. ## Load and mount ```js import { Hlpd } from "./hlpd/src/index.js"; const hlpd = await Hlpd.load({ apiKey: "hk_…", bundleURL: "/hlpd-bundle/", // baseURL defaults to https://api.hlpd.uk tickets: { serverURL: "https://api.hlpd.uk", projectID: "brp_your_product" }, context: () => ({ screen_name: "Settings" }), onEscalate: (ticketId, summary) => { /* Observe a sent ticket or handle app-owned support. */ }, }); const help = hlpd.mountHelpCenter(document.querySelector("#help")); const support = hlpd.mountSupport(document.querySelector("#support")); // help.unmount(); support.unmount(); hlpd.destroy(); ``` Both mounts render into shadow roots with the bundled stylesheet. Light and dark themes follow `prefers-color-scheme`, and dialogs keep drafts and close with Escape. Topics and search use only bundle files, so the help stays available when the API cannot be reached. `Hlpd.load` fetches the manifest and lexical index first and loads the embedding files in the background. `await hlpd.semanticReady` waits for hybrid search (it resolves to `false` if embeddings are absent or invalid), and `hlpd.orderReady` resolves to the fetched order, or `null`. ## Search and suggestions ```js const hits = hlpd.search("pause reminders", 5); // [{ id, title, summary, score, lexical, semantic }] const suggestions = hlpd.suggestions("reminders keep buzzing at night"); ``` ## Suggestions in your own form Give the SDK your input or textarea and an empty element above it. Suggestions wait 400 ms and need three content words plus the bundle's confidence threshold. ```js const attachment = hlpd.attachSuggestions(descriptionInput, suggestionsElement, { surface: "bug_report", }); // After your own report submission succeeds, before resetting the form: attachment.reportFiled({ ticketId: response.ticket_id }); // attachment.detach(); ``` Opening a card shows the topic and its walkthrough modes in an overlay. `reportFiled` records the suggested and opened topics with your ticket id and resets tracking for the next report. ## Your own chat UI `hlpd.chat` is an observable headless model. Its `entries` contain `{ id, role, text, attachments, isError? }`, with attachment types `topic`, `walkthrough`, `escalation` and `ticket`. ```js const unsubscribe = hlpd.chat.subscribe(model => renderYourChat(model)); await hlpd.chat.send("Where can I pause reminders?"); hlpd.chat.openSupport(); hlpd.chat.updateDraft(draftId, { title: "Reminders won't stop", summary: "Details…" }); await hlpd.chat.sendSupport(draftId); hlpd.chat.cancelDraft(draftId); hlpd.chat.openedTopic(topicId); hlpd.chat.viewedWalkthrough(topicId); // hlpd.chat.reset(); unsubscribe(); ``` With no tickets configuration, the chat says "Support is handled by the app" and calls `onEscalate(null, summary)`. With tickets, it calls `onEscalate` only after Send succeeds. `hlpd.agent.ask(question)` returns raw agent replies without submitting tickets. ## Tickets `hlpd.tickets` exposes `list()`, `ticket(id)`, `refresh()`, `refreshTicket(id)`, `markRead(id)`, `messages(id)`, `reply(id, body)` and `subscribe(listener)`, plus `submitReport({ title, text, manifest, context, topicsOpened, attachments })` for host forms that share the SDK inbox. Attachments are `File` or `Blob` objects: PNG, JPEG, WebP or GIF, at most 5 MB each. ## Report widget `src/bug-reporter.js` and `src/bug-reporter.css` also work on their own as a report and feature-request form for any page. Mark up a form and it mounts automatically: ```html
``` Or call `mountBugReportForm(form, { serverUrl, projectId })` yourself. The widget lists the visitor's saved tickets and threads. `widget/index.html` in the SDK is a complete example with a launcher and dialog. ## Walkthroughs Pass a `walkthroughs` host to `Hlpd.load`. Modes are `showMe` and `guideMe`; the optional `supports(script, mode)` can return `false` to disable a mode. `begin` returns a session, and the exported `createDOMSession` builds one from a registry of your real elements: ```js import { Hlpd, createDOMSession } from "./hlpd/src/index.js"; const hlpd = await Hlpd.load({ apiKey: "hk_…", bundleURL: "/hlpd-bundle/", walkthroughs: { async begin({ walkthrough, mode, container, signal }) { if (mode === "showMe") { const demo = createDemoLogView(); // your own view with disposable sample data container.append(demo.root); return createDOMSession({ root: demo.root, registry: new Map([["record-drink", demo.recordButton]]), isolated: true, dispose: () => demo.root.remove(), }); } return createDOMSession({ root: document.querySelector("#app"), registry: new Map([["record-drink", document.querySelector("#record-drink")]]), }); }, }, }); ``` - Show me requires `isolated: true` and a newly created view with disposable data, mounted in `container`. Never connect it to production data. - Guide me returns the real page root. The SDK never calls `perform` in Guide me; actionable steps advance only after the user's own tap or typing. - The SDK never executes selectors or bundle code. Targets resolve only through your registry. Narration audio is generated in advance by the publisher and plays by default, with Mute and Enable sound controls. Captions stay visible if the browser blocks autoplay. ## Device id The SDK generates one 16-character base62 device id per browser storage and keeps it under `Hlpd.deviceID`. Help requests send it as `X-Hlpd-Device` and reports send it as `client_id`. `deviceID(storage)` from `src/storage.js` reads it. # Generator Source: https://hlpd.uk/docs/generator `hlpd_generator` reads an app's source, drives its UI and builds a help bundle: topics, Show me and Guide me walkthroughs with narration, captioned videos and an on-device search index. ## Set up ```sh cd generator uv venv -p 3.12 .venv && VIRTUAL_ENV=.venv uv pip install -r requirements.txt ``` Settings come from the environment, `.env` or `~/.env`: | Variable | Purpose | |---|---| | `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL` | The language model gateway that writes topics and scripts | | `OPENROUTER_API_KEY` | Narration audio. Or set `GEMINI_API_KEY` with `HLPD_TTS_PROVIDER=gemini` | | `HLPD_IOS_SIMULATOR` | The simulator used to record iOS walkthroughs | | `HLPD_EMBEDDING_MODEL` | The search embedding model, `minishlab/potion-retrieval-32M` by default | | `HLPD_API_URL` | The hlpd API used by `publish`, `https://api.hlpd.uk` by default | The other `HLPD_*` settings are in `hlpd_generator/config.py`. ## Generate ```sh .venv/bin/python -m hlpd_generator generate ~/Code/drink_ios --platform ios --out /tmp/hlpd/drink ``` Stages run in order, and each skips work whose outputs already exist: | Stage | What it does | |---|---| | `analyze` | Builds a product model of the app from its source | | `outline` | Plans categories and topics, and which topics can have an in-app walkthrough | | `docs` | Writes each topic in Markdown | | `enrich` | Gives each topic more ways to be found, and held-out queries that measure search | | `scripts` | Writes a step script for each topic with a feasible walkthrough | | `translate` | Localises the help into the app's own locales | | `narrate` | Speaks each step's narration and converts it to AAC | | `walkthrough` | Compiles the steps into bounded, declarative in-app walkthroughs | | `record` | Runs each script through the platform driver and records the screen | | `review` | Checks narration against the recorded screen and corrects it | | `render` | Turns recordings into captioned videos with poster frames | | `bundle` | Assembles the bundle and checks it | - `--stage --force` redoes one stage. - `--only id,...` limits per-topic work to the listed topics. - `--languages es,fr` overrides the translated locales. - `--gaps queries.json` assigns unanswered user queries to topics. Recording uses the iOS simulator and the `HlpdDriver` UI-test runner from the iOS SDK. The finished bundle is in `/bundle`. ## Edit and reindex After editing topics by hand, rebuild search, embeddings, suggestion thresholds and the manifest's `files` inventory: ```sh .venv/bin/python -m hlpd_generator reindex /tmp/hlpd/drink/bundle ``` ## Publish ```sh HLPD_EMAIL=me@example.com HLPD_PASSWORD=... \ .venv/bin/python -m hlpd_generator publish /tmp/hlpd/drink/bundle --project brp_... ``` `publish` validates the bundle, uploads it as a gzipped tar to `POST /admin/api/projects//bundles` and prints the result. It uses the owner session in `HLPD_TOKEN`, or opens one with `HLPD_EMAIL` and `HLPD_PASSWORD` and closes it afterwards. `--api URL` overrides `HLPD_API_URL`, and `--draft` uploads without making the bundle live. ## Bundle layout ```text HlpdBundle/ manifest.json topics/.md walkthroughs/.json declarative in-app walkthrough audio//.m4a pre-generated AAC narration videos/.mp4 rendered walkthrough video videos/.vtt captions, one cue per step images//1.png video poster frame search.json precomputed inverted index embeddings/ static embedding model and chunk vectors ``` `manifest.json` describes the product, categories and topics, and ends with a `files` inventory holding the SHA-256 and size of every other file. Validation requires the inventory to match the directory exactly, and over-the-air updates use it to fetch only the changed files. An abridged manifest: ```json { "schema": 1, "bundleId": "drink-ios", "version": "2026.10.12-1", "product": { "name": "Drink", "platform": "ios", "appBundleId": "uk.ph1l.drink", "appVersion": "1.4.0" }, "categories": [{ "id": "getting-started", "title": "Getting started", "order": 1 }], "topics": [{ "id": "log-a-drink", "title": "Log a drink", "summary": "Record a glass of water from the home screen.", "category": "getting-started", "doc": "topics/log-a-drink.md", "walkthrough": "walkthroughs/log-a-drink.json" }], "files": { "topics/log-a-drink.md": { "sha256": "9f2c…", "bytes": 812 } } } ``` # Server API Source: https://hlpd.uk/docs/server Everything the SDKs, the report widget and the generator send goes to `https://api.hlpd.uk`. These are its public endpoints and what they expect. ## SDK API Requests to `/v1/*` carry the app's SDK key and identify the bundle and device: ```http Authorization: Bearer hk_… X-Hlpd-Bundle: drink-ios X-Hlpd-Bundle-Version: 2026.10.12-1 X-Hlpd-Device: 7fQ2kLp9Xc3mVb8N ``` | Endpoint | Purpose | |---|---| | `POST /v1/chat/completions` | OpenAI-style chat completions for the on-device agent, within the app's daily, monthly and per-device limits | | `POST /v1/events` | Analytics events as `{"events": [...]}`, at most 200 per request | | `GET /v1/order` | Topic order and service-notice pins, optionally for one `screen`, with `ETag` support | | `GET /v1/bundle` | The live published manifest for over-the-air updates, or `204` when none is live | | `GET /v1/files/{sha256}/{name}` | One bundle file by content hash | Event types are `topic_view`, `search`, `walkthrough_start`, `walkthrough_complete`, `chat_question`, `feedback`, `suggestion_shown`, `suggestion_opened` and `report_filed`. The chat agent runs on the device and the API only gates the language model. The agent uses plain tool calling with `search_topics`, `get_topic`, `show_topic`, `show_walkthrough` and `escalate`, with at most 6 tool rounds and 600 tokens per completion. ## Reports and tickets Reports and feature requests are sent as `multipart/form-data` to `POST /api/reports`: | Field | Notes | |---|---| | `project_id` | The public project id, `brp_…` | | `title` | Required | | `kind` | `bug` (default) or `feature` | | `body` | The description | | `client_id` | The device id. Required when attaching files | | `classification_level` | `low`, `medium` (default) or `high` | | `metadata` | A JSON object as a string, such as the screen, app version and topics viewed | | `attachments` | Images, up to 4 files of 5 MB each | ```sh curl https://api.hlpd.uk/api/reports \ -F project_id=brp_your_project \ -F title="Reminders won't stop" \ -F kind=bug \ -F body="They keep buzzing after 10pm." \ -F client_id=7fQ2kLp9Xc3mVb8N \ -F 'metadata={"screen_name": "Settings"}' ``` The response identifies the ticket. `reply_token` is only returned when the app has public ticket status turned on; keep it to read and reply to the thread. ```json { "id": "…", "ticket_id": "…", "ticket_url": "https://api.hlpd.uk/tickets/…", "client_id": "7fQ2kLp9Xc3mVb8N", "kind": "bug", "status": "received", "classification_level": "medium", "status_visible": true, "can_reply": true, "reply_token": "…" } ``` | Endpoint | Purpose | |---|---| | `GET /api/tickets/{ticket_id}` | Ticket status and public messages | | `POST /api/tickets/{ticket_id}/messages` | Reply as the reporter with `{"reply_token": "…", "body": "…"}` | | `POST /api/tickets/{ticket_id}/push` | Register for reply pushes with `{"reply_token": "…", "platform": "apns", "token": "…"}`; `platform` is `apns` or `fcm` | | `POST /api/tickets/{ticket_id}/push/unregister` | Stop reply pushes | | `GET /report/{project_id}` | The hosted report form | | `GET /tickets/{ticket_id}` | The hosted ticket status page | When an app has allowed origins, browser requests must come from one of them. ## Owner API The admin, the native admin app and the generator use owner endpoints under `/admin/api`. Native apps and command-line tools open a bearer session, valid for 30 days, with `POST /admin/api/session` and `{"email": "…", "password": "…"}`. Coding agents can create an account and first app with `POST /api/signup`; see [For coding agents](https://hlpd.uk/docs/agents). | Endpoint | Purpose | |---|---| | `GET /admin/api/projects` | Your apps | | `POST /admin/api/projects` | Create an app within your plan's app limit | | `PATCH /admin/api/projects/{id}` | Bundle id, quotas and store text | | `POST /admin/api/projects/{id}/keys` | Create an SDK key | | `DELETE /admin/api/projects/{id}/keys/{key}` | Revoke an SDK key | | `POST /admin/api/projects/{id}/bundles` | Upload a bundle as a gzipped tar | | `GET /admin/api/projects/{id}/bundles` | List uploaded bundles | | `PUT /admin/api/projects/{id}/bundle` | Choose the live bundle version | | `GET /admin/api/projects/{id}/usage` | Chat usage against the app's limits | | `GET /admin/api/projects/{id}/analytics` | Help analytics; `topics` and `gaps` sit alongside it | | `GET /admin/api/projects/{id}/pins` | Service-notice pins; `PUT` replaces them, with `ETag` | | `GET /admin/api/reports` | Reports across your apps | `GET /health` reports whether the server is up. ## Accounts and email Owners sign in with an email address and a password. When an account is created, hlpd sends a verification email. The account works straight away; upgrading to a paid plan needs a verified email address. | Endpoint | Purpose | |---|---| | `POST /api/signup` | Create an account and first app with `{"email": "…", "password": "…", "project": "…"}`; see [For coding agents](https://hlpd.uk/docs/agents) | | `POST /api/verify` | Verify the email address with `{"token": "…"}` from the verification email | | `POST /api/verify/resend` | Send the signed-in owner's verification email again | | `POST /api/password/forgot` | Send a password reset email with `{"email": "…"}` | | `POST /api/password/reset` | Set a new password with `{"token": "…", "password": "…"}` from the reset email | ## Run your own server The server is a FastAPI app with one SQLite database. To deploy it to a Linux host running Caddy: 1. Copy `deploy/env.example` to `/opt/hlpd/server/.env` on the host and fill it in. 2. Point the API domain at the host. Caddy obtains the certificate. 3. Run `deploy/server.sh user@host`, and again after every change. At startup the server creates two accounts if they are missing: a super-admin from `HLPD_ADMIN_EMAIL` and `HLPD_ADMIN_PASSWORD`, and an owner from `HLPD_OWNER_EMAIL` and `HLPD_OWNER_PASSWORD`. Verification and password reset emails use these settings: | Variable | Purpose | |---|---| | `HLPD_MAIL_BACKEND` | `log` writes each email to the server log instead of sending it; `ses` sends it through Amazon SES | | `HLPD_MAIL_FROM` | Sender address, which must be a verified SES identity when sending through SES | | `HLPD_MAIL_REPLY_TO` | Reply-to address | | `AWS_REGION` | The SES region | With `ses`, also provide AWS credentials allowed to send through SES, such as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. Data lives in the service user's `~/.hlpd`: the SQLite database, uploads and bundle objects.