Demo builds
A demo build is a throwaway assistant built from a prospect's public website, so an agency can show a practice what the product would do for it before anything is installed. The agency starts one from the Agency Console or through the partner API; a few minutes later there is an unlisted test page with the prospect's homepage behind a live widget, plus a list of common questions the assistant could not answer — the gaps the agency offers to fill.
- API contract and GoHighLevel recipe: Partner API
- Design decisions and slice history:
docs/plans/ProvisioningAPIPlan.md - Test tools:
tools/partner-api/(see Testing below)
Who can do what
| Action | Who |
|---|---|
| See the Demo builds list | Any agency member |
| Start or delete a demo, edit the page wording | Agency managers |
| Create, edit or revoke API keys | Agency managers |
| Start, read or delete demos over the API | Anyone holding an active key |
| See demo workspaces in the central console | Central admins, with "Show demo workspaces" ticked |
What a build does
A build is a crawl_jobs job of kind demo, run by cmd/renderer (the only
process with a headless browser). The renderer scales to zero, so the first build
after an idle period waits about a minute for it to boot; the console shows this
as "Waiting for a build worker to start".
- Create (chatbot-server, synchronous): validate the URL (public http(s)
only), check the limits, create a demo tenant (
tenants.is_demo=1, planpro, pre-chat form off, AI answers immediately) and ademo_buildsrow, mint the test-page token, enqueue. - Configure: apply the industry playbook (therapy by default), then switch the crisis gate on with the region's lines — 911 and 988 for both US and CA.
- Crawl at most 15 pages. robots.txt is respected. This is the one crawl in the product that does not require a verified domain; the page cap is the mitigation.
- Ingest the pages (plus the site footer) as one knowledge document.
- Screenshot the homepage at 1280×800, stored with the tenant's uploads.
- Ask the fixed questions cold, with no tools: an empty grounded answer is "not answered". Therapy asks about insurance, openings, ages, telehealth and fees; other industries ask about hours, location, services, pricing and contact.
- Finish:
readywith results, orerrorwith a reason code.
Failures nobody can fix by retrying — the site blocks us, has no readable text, or will not render a screenshot — are final at once. Retrying would only crawl somebody else's site twice more. Infrastructure failures are retried by the queue, and every step is safe to repeat.
| Reason | Meaning |
|---|---|
crawl_blocked |
robots.txt, 403s or a bot wall on every page |
no_content |
Pages loaded but had no readable text |
screenshot_failed |
The homepage would not render |
ingest_failed |
The knowledge base could not be built |
internal / timeout |
Our side; start a new build |
The demo page
Same /test-page/{token} link as the ordinary test page; a workspace with a
demo_builds row gets the demo layout instead (handlers/testpage_demo.go):
- A "Preview" banner — the agency's wording or "Test page for {business}. Built from your public website. Not live."
- The homepage screenshot in a browser frame, with the real widget on top. The widget claims the prospect's homepage URL, so page-aware answers match.
- Up to three starter questions, taken from the ones it could answer, so the
first click works. They send through
ChatWidgetTest.ask()(widget 1.0.66+). - "What it couldn't answer yet" with the unanswered questions and, if the agency set one, a booking button.
- Copy in EN/FR/NL/DE by the visitor's browser language (or
?lang=).
The page returns 404 while the build is running, after a failure, and from
the moment it expires. It sends X-Robots-Tag: noindex, nofollow.
Agencies set their own wording under Demo builds → Page wording: banner,
the text above the unanswered questions, button label and link. {business} is
replaced per demo; a blank field uses the default.
Guardrails
- No contact details. The pre-chat form is off, lead capture is off, the AI
is not offered
remember_contact_details, and the visitor transcript email is disabled (TenantHub.isDemo). - No alerts. Chats are Test chats (
is_test), so SMS escalation and lead routing are already suppressed; a demo has no operators, devices or lead destinations, so push, missed-chat email and webhooks have no one to reach. - Crisis safety stays on. Every demo gets the crisis gate, whatever its
industry. The message lists 911 and 988, says nobody is watching the chat, and
promises no follow-up (
safety.Unmonitored). - Not a customer. Demo tenants are not agency subaccounts: they never touch the site pool, consolidated billing, white-label or staff access, and the central console hides them by default.
Limits
- 50 builds per agency per rolling 24 hours.
- One build of the same site (registrable domain) per agency per 10 minutes.
- 60 API requests per minute per key.
- 15 pages per build.
Expiry and deletion
Demos live 14 days. The link 404s at expiry; within the hour, cmd/worker's
sweep (DemoHandler.PurgeExpired) permanently deletes the tenant: knowledge base,
every Qdrant collection, chats and uploaded files. The same sweep removes demo
tenants whose build row has gone (what deleting an agency leaves behind).
Deleting a demo from the console or with DELETE /v1/demos/{id} does the same
immediately.
Partner API and callbacks
Summary only — see Partner API. Keys are created by agency
managers, expire within 6 months, and carry a webhook signing secret. Builds
started with a key and a callback URL are POSTed back by cmd/worker when they
finish, signed X-ChatOps-Signature: t=…,v1=…, retried for about 10 hours. Each
build in the console shows "callback pending / sent / failed".
Where things live
| Piece | Location |
|---|---|
| Tables | demo_builds, partner_api_keys, tenants.is_demo, organizations.demo_* (migrations 154, 155) |
| Create, run, delete, expiry | internal/handlers/demo.go |
| Demo page | internal/handlers/testpage_demo.go, assets/testpage_demo.html, demo_page_copy.go |
| API + key management | internal/handlers/partner_api.go, internal/partnerapi |
| Callback delivery | internal/handlers/demo_callback.go (run by cmd/worker every 10 s) |
| Screenshot, page ingest | internal/poller/demo.go |
| Console UI | dashboard/src/pages/AgencyDemos.jsx, AgencyApiKeys.jsx |
Testing
API, scripted
.\tools\partner-api\test-demos.ps1 -ApiKey cop_live_… -WebsiteUrl https://www.some-practice.com `
-CallbackUrl https://webhook.site/<your-id> -Delete
Runs the refusals first (no key, bad key, http callback, private address,
unknown region, unknown id), then a real build: idempotency on externalRef,
polling to completion, the result, lookup by reference, the X-API-Key header,
and — with -Delete — that deletion makes the link 404. Exits non-zero on any
unexpected response. A build that ends failed with a reason is reported, not
counted as a test failure: plenty of real sites block crawlers.
API, by hand
tools/partner-api/demos.http has every request, positive and negative, for
the VS Code REST Client extension. Paste a key into @apiKey (and don't commit
it).
Callback signature
Save the callback body byte-for-byte from webhook.site, then:
node tools/partner-api/verify-signature.js --secret=whsec_… \
--header="t=…,v1=…" --body=callback.json --tolerance=0
--sign produces a valid header for a body, to test your own receiver.
Console and page
- Agency Console → Demo builds → New demo on a real practice site. Watch it go waiting → reading pages → ready.
- Open the test page. Check the banner, the screenshot, the starter questions (no form appears), the gaps panel and button, and the phone layout.
- In the chat: a crisis phrase ("I don't want to be alive anymore") must list 911 and 988 and say nobody is watching; "My name is Test, test@example.com" must not be saved (the visitor row stays without a name or email).
- A site that blocks crawlers must end
crawl_blocked, with no link.
Expiry, without waiting 14 days
UPDATE demo_builds SET expires_at = NOW() - INTERVAL 1 MINUTE WHERE external_ref = '<ref>';
The link 404s at once. Restart cmd/worker (the sweep runs at start-up, then
hourly) and check its log for demo expiry: deleted N demo(s), that the tenant
row is gone, that no kb_<tenantId> collection remains (central console → Ops),
and that <tenantId>/ is empty in the uploads bucket.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Stuck on "Waiting for a build worker" for 10+ min | Renderer not starting; the job is marked abandoned at 10 min. Check renderctl in the worker log. |
| Every agency endpoint returns "db error" | A migration added a column to organizations without a field on models.Organization (it is loaded with SELECT *). TestOrganizationStructCoversEveryColumn catches this. |
| "API keys are not available on this server" | INTEGRATION_ENCRYPTION_KEY is not set on chatbot-server. |
| Callback status "failed" | The receiver did not return 2xx within ~10 h of retries; the last error is in the badge's tooltip and demo_builds.callback_last_error. |
| Callbacks never sent | INTEGRATION_ENCRYPTION_KEY missing on cmd/worker (it signs them), or the build was not made with a key. |
| Starter questions do nothing | Widget older than 1.0.66 (no ChatWidgetTest.ask). |