Headless SDK
Same public key as the widget, same knowledge, same plan — without the launcher. Put an input on a help page, a docs site or inside your app, and render the answer however you like. Conversations land in your inbox next to widget chats, so ratings, leads and human handoff keep working. The page's domain must be in the bot's Allowed domains, exactly as for the widget.
No build step — one tag
<script async src="https://app.matterchat.co/askbox.js"
data-key="YOUR_BOT_KEY"
data-suggestions="Pricing|Refunds|Shipping"></script>Renders a search-style ask box where the tag sits, in its own shadow root, so your theme and it leave each other alone. Works on Docusaurus, Mintlify, GitBook, Notion-exported sites, WordPress — anywhere you can paste HTML. Options: data-target="#selector" to render elsewhere, data-placeholder, data-accent="#hex", data-unbranded. Pin it with /askbox/<hash>.js from the build if you want Subresource Integrity.
Or a package, for your own UI
pnpm add @matterchat/core # framework-free
pnpm add @matterchat/react # or vue, svelte, angular — each exports useMatterChatOne question, one streamed answer
import { createMatterChat } from "@matterchat/core";
const mc = createMatterChat({ publicKey: "pk_live_…" });
input.addEventListener("focus", () => mc.warm(), { once: true });
const answer = await mc.ask("Do you ship to Canada?", {
onToken: (_chunk, full) => (output.textContent = full),
});
answer.sources; // [{ n: 1, title: "Shipping", url: "https://…" }]A help page in React
import { useMatterChat } from "@matterchat/react";
export function AskBox() {
const { messages, ask, streaming, error, status, warm } = useMatterChat({ publicKey: "pk_live_…" });
return (
<form onSubmit={(e) => { e.preventDefault(); ask(e.currentTarget.q.value); e.currentTarget.reset(); }}>
<input name="q" placeholder="Ask anything about our product" onFocus={warm} disabled={streaming} />
{status === "connecting" && <p>Connecting…</p>}
{messages.map((m, i) => (
<p key={i} data-role={m.role}>
{m.content}
{m.sources?.map((s) => <a key={s.n} href={s.url ?? "#"}>[{s.n}]</a>)}
</p>
))}
{error && <p role="alert">{error.message}</p>}
</form>
);
}useMatterChat keeps one conversation: the question appears at once, the answer grows token by token, and follow-ups remember what was asked. rate(1 | -1) rates the latest answer; reset() starts over and stops an answer still streaming, as leaving the page does. Nothing connects until warm() or the first question, and status reads connecting meanwhile. Render answers with parseBlocks(text), the widget's own parser: paragraphs, lists, bold, code, links and citations as typed tokens. Pass pageUrl: "path" or "none" to send less of the page address. Vue, Svelte and Angular expose the same shape as a composable, a store and signals.
What an answer tells you
text | The answer, as Markdown — render it with the same renderer as the rest of your page. Streams through onToken first. |
sources | The pages the answer cited, once each, in the order it mentions them. Render these under it. |
retrieved | Every passage it could cite, numbered as its [n] markers are. Resolve inline markers against this. |
refusal | Nothing relevant was found; the bot said so instead of guessing. |
lowConfidence | An answer from thin evidence — a good moment for “was this helpful?”. |
handoff | The visitor asked for a person and your team was paged. |
humanMode | A teammate owns this conversation; their reply arrives in the inbox, text is empty. |
ask() throws a MatterChatError with a code: rate_limited, capped (plan limit), not_allowed (the message names the domain that is not listed; add localhost while you build), insecure_context (the page is plain HTTP — attestation needs HTTPS or localhost), unavailable, network. The hooks catch these into errorand keep the visitor's question on screen.