TaifaSupport Docs
Widget

Installing the widget

One script tag on a portal. The tracker that feeds the live visitor list and the chat surface the visitor talks through.

The widget is the script an Institution pastes into its website. It does two jobs at once: it is the tracker that feeds the live visitor list, and it is the chat surface the visitor talks through.

One file, no runtime dependencies, no framework, roughly 9 KB gzipped.

Install

<script src="https://support.acme.go.ke/widget.js" data-site="ts_abc123"></script>

That is the whole installation. Put it before </body> on every page.

The exact snippet for a site, with its own key already filled in, is on the Sites screen and is returned by GET /v1/sites/{site_id}/snippet.

Attributes

AttributeRequiredWhat it does
data-siteyesThe site key from Settings, Websites. Public by design: it only authorises anonymous visitor traffic for one site.
data-apinoAPI root including the version segment, for example https://api.acme.go.ke/v1. Defaults to the origin the script was served from plus /v1.
data-consentnorequired holds everything until grantConsent() is called. granted declares that the site handles consent itself and skips the bootstrap round trip. Omitted means the site's own tracking_consent_required setting decides.
data-debugnoLogs to console.debug. Without it the widget is completely silent, including when the backend is down.

Loading it asynchronously

If a tag manager loads widget.js with async, queue calls so nothing is lost before the bundle arrives:

<script>
  window.TaifaSupport = window.TaifaSupport || { q: [] };
  window.TaifaSupport.q.push(["identify", { id: "183", name: "James Mwangi" }]);
</script>
<script async src="https://support.acme.go.ke/widget.js" data-site="ts_abc123"></script>

The queue is drained in order as soon as the bundle boots.

It cannot break the host page

This is a design constraint rather than a claim, because the widget runs on someone else's page, often a government portal that nobody wants to be responsible for taking down.

  • Every network call resolves to null rather than rejecting. There is no unhandled rejection to surface in the portal's own error tracking.
  • Every storage call is wrapped. Safari private mode throws on localStorage access, not only on write.
  • Every listener runs inside a guard. A listener that throws is caught and logged, never rethrown into the page.
  • The whole boot is inside one try/catch.
  • A circuit breaker opens after three consecutive failures and the widget stops touching the network for up to five minutes, so an outage cannot become a retry storm from every visitor at once.
  • With data-debug absent, nothing is ever written to the console.

Shadow DOM isolation

The whole UI lives in a shadow root with all: initial on the host, so the portal's CSS cannot reach in and the widget's cannot leak out. If a browser has no attachShadow, the UI does not mount and the tracker keeps working: a visitor on an ancient browser still appears in the live list, they just cannot chat.

Single-page applications produce pageviews

history.pushState and replaceState are patched, preserving their return value and receiver, and popstate and hashchange are handled.

A burst of router calls collapses into one pageview for the final URL, so a transition that writes history three times does not report three pages. That matters because page_view_count is a column an agent reads on the live row, and inflating it makes the row lie.

Accessibility

  • The launcher is a real button with aria-haspopup, aria-expanded and an accessible name that includes the unread count.
  • The panel is role="dialog" with aria-modal="false" and no focus trap. The visitor is still on your page, and trapping them in a support widget would be hostile.
  • Escape closes the panel and returns focus to the launcher.
  • The transcript is role="log" with aria-live="polite". The typing indicator announces through a visually hidden status region.
  • prefers-reduced-motion is respected.

On a narrow viewport, opening the panel focuses the panel rather than the composer, so the on-screen keyboard does not cover the transcript before anything has been read.

Endpoints it calls

CallWhen
GET /v1/track/widget/{site_key}Boot, unless data-consent="granted".
POST /v1/track/sessionOnce per page load. Returns the visitor token, session id and widget settings.
POST /v1/track/pageviewThe landing page, then every SPA route change.
POST /v1/track/heartbeatEvery 25s while the tab is visible, and once on pagehide as a keepalive request, or sendBeacon where keepalive is unsupported.
POST /v1/track/identifyOn identify(), once per session per distinct set of traits.
POST /v1/track/eventOn track(), chat opened, invitation accepted or dismissed.
WS /v1/ws/widget?site_key=&session_id=Agent replies, typing, proactive invitations, and outbound visitor messages.
POST /v1/track/messageFallback only, when the socket is not open.

Full request and response shapes are in the track reference.

Next

On this page