Hopp til innholdet

DokumentasjonSDK (headless)Markdown

Headless with the SDK

@snakkbar/sdk is a small, dependency-free TypeScript client for the HTTP API, plus optional React hooks. Use it to build your own chat UI, your own staff inbox, or both.

Install

ts
// From the Snakkbar host (no install):
import { createVisitorClient, createOperatorClient } from "https://<host>/sdk/snakkbar.mjs";
import { useVisitorChat } from "https://<host>/sdk/react.mjs"; // needs `react` in your import map/bundle

// Or vendor packages/sdk from the Snakkbar repo (npm package `@snakkbar/sdk`, ESM + .d.ts in dist/).

All types (Message, TurnEvent, OperatorConversation, …) are exported. The full surface is packages/sdk/src/interface.ts; the wire types are packages/sdk/src/types.ts.

Visitor side

ts
const chat = createVisitorClient({
  host: "https://<host>",
  tenantId: "amatec",
  // token: identifiedVisitorToken,  // optional, see widget.md → Identified visitors
  // language: "en",                  // default: browser language
});

const config = await chat.config();            // assistant name/portrait, welcome, team, presence
const conversation = await chat.createConversation({ pageUrl: location.href, pageTitle: document.title });

let draft = "";
const result = await chat.send(conversation.id, { body: "Har dere sveisemaskiner?" }, {
  onEvent(event) {
    switch (event.type) {
      case "message":          render(event.message); break;          // the visitor's stored message
      case "assistant.status": showStatus(event.label); break;         // "Søker etter produkter"
      case "assistant.delta":  draft += event.text; renderDraft(draft); break;
      case "assistant.done":   draft = ""; event.messages.forEach(render); break; // text, product cards, suggestions
      case "silent":           break; // a person owns the chat or was asked for; their reply arrives via subscribe()
      case "error":            showError(event.message); break;
    }
  },
});

// Live updates: staff replies, typing, read receipts, presence.
const sub = chat.subscribe(async (event) => {
  if (event.type === "message" || event.type === "conversation") rerender(await chat.getConversation(conversation.id));
  if (event.type === "typing" && event.side === "operator") showTyping(event.operator?.name, event.typing);
  if (event.type === "presence") updateTeam(await chat.config({ fresh: true }));
}, { conversationId: conversation.id });

Other visitor calls: listConversations(), getConversation(id), upload(id, file) then send(id, { body, attachmentIds }), markRead(id, isoTime), setTyping(id, bool) (throttled for you), requestHuman(id, handles?), requestCallback(id, { name, phone | email, message }), reset().

Visitor profile and live visitors

On a headless site (no stock widget), start tracking once per page load and tell Snakkbar who the visitor is when you know:

ts
const stopTracking = chat.startTracking(); // heartbeats, page changes (also SPA routes), live view

// After login on your site (or whenever you learn more):
await chat.updateProfile({
  name: user.name,
  email: user.email,
  phone: user.phone,
  company: user.company,
  attributes: { customerNo: user.customerNo, plan: "pro", newsletter: true }, // null removes a key
});
const me = await chat.me(); // VisitorProfile: what Snakkbar knows, `verified` for identified visitors
  • startTracking(options?) keeps the visitor visible to staff while the page is open: a heartbeat about every 10 s with URL, title, visibility, viewport and scroll; an immediate report on page loads, single-page-app URL changes and tab switches; and, while an operator watches, pointer and click samples every ~500 ms. It never sends page content. It returns a stop function; call it once per client (a second call is a no-op until you stop the first).
    • Checks config().liveVisitors first and does nothing when the tenant has it off — and then it doesn't create an anonymous visitor either.
    • Inside an operator's live view (an iframe named snakkbar-mirror) it reports nothing and runs the mirror instead (see admin-embed.md). So call it on every page, also if you only want the live view to work.
    • Options: url: () => string reports another URL than location.href (e.g. without private path segments); onWatch(watched) is called when an operator starts or stops watching.
    • With an open subscribe() the tracker hears about a watch at once; otherwise at its next heartbeat (≤ 10 s).
    • Pages that run the stock widget already track; don't start a second tracker there.
  • updateProfile(input) → PATCH /visitor/me. Calls within 250 ms are merged into one request. Unverified: for an identified visitor it only fills fields that are still empty and adds attribute keys that don't exist yet (send trusted values from your backend with the visitor token instead). Rules in api.md.
  • activity(input) sends one raw heartbeat/batch; you only need it if you write your own tracker.

Rendering rules the stock widget follows (match them for a consistent experience):

  • role: "assistant" bodies are Markdown; everything else is plain text.
  • Show message.translation.body when present, with a way to see body.
  • kind: "products" → cards from message.products; kind: "suggestions" → chips from message.suggestions that send the chip text when tapped.
  • kind: "navigation" → move the browser to message.navigation.url if it is same-origin (once per message id).
  • System kinds (joined, left, requested, callback, closed) render as small notices.
  • Show «Sett» under the visitor's latest message when conversation.teamReadAt ≥ its createdAt.
  • Send setTyping(id, true) while the visitor types and false on send/blur.

React

tsx
import { createVisitorClient } from "@snakkbar/sdk";
import { useVisitorChat } from "@snakkbar/sdk/react";

const client = createVisitorClient({ host: "https://<host>", tenantId: "amatec" });

export function Chat() {
  const { config, messages, streamingText, status, send, startNew } = useVisitorChat(client);
  // render messages, streamingText (assistant reply in progress), status ("idle" | "sending" | …)
}

Staff side

ts
const staff = createOperatorClient({
  host: "https://<host>",
  token: async () => (await fetch("/api/snakkbar/operator-token", { method: "POST" }).then((r) => r.json())).token,
});

const { operator, tenant } = await staff.me();
const stopPresence = staff.startPresence();          // heartbeats while the page is visible
await staff.updateMe({ available: true });           // check in

const { conversations } = await staff.listConversations({ filter: "open" });
const detail = await staff.getConversation(conversations[0].id);
await staff.act(detail.id, "join");
await staff.send(detail.id, { body: "Hei! Jeg ser på dette nå." });
await staff.send(detail.id, { body: "@amanda hva er leveringstiden på X200?" }); // ask the assistant

staff.subscribe((event) => {
  // "conversation" / "message" → refetch that conversation (conversationId "*" = refetch everything,
  // sent while the SDK is polling because streaming is unavailable)
  // "typing" (side "visitor") → typing indicator; "presence" → refetch team()
});

Also: act(id, "takeover" | "release" | "leave" | "close" | "reopen"), navigate(id, url), upload, markRead, setTyping, deleteConversation (admin), requests(), team(), updateMe({ greeting, language, displayName }), uploadAvatar(file), knowledge.*, callbacks.*, tenant.* (admin), apiKeys.* (admin), dashboard() and visitors.* (below).

Overview and live visitors

ts
const stats = await staff.dashboard({ days: 30 });  // Dashboard: online now, conversations, response time, top pages…

const visitors = await staff.visitors.list({ minutes: 30 }); // LiveVisitor[], online first
const visitor = await staff.visitors.get(visitors[0].id);    // + pageviews (navigation history) and conversations

// Live view: while watched, the visitor's browser streams cursor, clicks and scroll.
const stopWatching = staff.visitors.watch(visitor.id);       // renews every 10 s until stopped
const sub = staff.subscribe((event) => {
  if (event.type === "visitor") refreshVisitorList();        // came online, new page, profile changed (debounce)
  if (event.type === "pointer" && event.visitorId === visitor.id) {
    // LiveFrame: url, title, visible, viewport, scroll, documentHeight,
    // samples/clicks as [t ms since batch start, x, y] in the visitor's viewport coordinates.
    drawFrame(event);
  }
});
// leaving the live view:
stopWatching();
sub.close();
  • dashboard({ days }): 1–90, default 7; see api.md for every field.
  • visitors.list({ minutes }): visitors seen in the last minutes (default 30, max 1440). online means a heartbeat in the last 35 s; there is no event when someone goes offline, so re-evaluate lastSeenAt on a timer (or refetch every 15–30 s) as well as on visitor events.
  • visitors.get(id): LiveVisitorDetail with up to 200 pageviews and 30 conversations.
  • visitors.watch(id) → stop function. Renews the watch every 10 s (it expires 25 s after the last renewal, so a closed tab stops it by itself). pointer events go to every operator's stream; filter by visitorId.
  • To show the visitor's page behind the cursor, load currentPage.url (or the frame's url) in an iframe named snakkbar-mirror and keep it scrolled with MirrorMessages; MIRROR_NAME, isMirrorMessage and the MirrorMessage type are exported. Recipe in admin-embed.md.

React: useOperatorInbox(client, { filter }), useOperatorConversation(client, id), usePresence(client).

Utilities

  • ALL_CONVERSATIONS ("*"): the conversationId the operator subscribe() uses while polling, meaning "refetch everything you show".
  • prepareImage(file): what upload() does before sending — photos over 1.5 MB or wider than 1600 px are re-encoded as JPEG (GIFs untouched). Use it if you upload through your own code.
  • knowledge.upload(file) sends the file straight to storage through a signed URL, so documents up to 50 MB work.

Errors

Every failed call throws SnakkbarError with status, a stable code (unauthorized, forbidden, not_found, rate_limited, origin_not_allowed, …) and a Norwegian message suitable for showing to the user.