One unified API for 18 live-chat & customer-support widgets. Zero deps, tree-shakeable, SSR-safe, CSP-aware, strict TypeScript.
ahize
One unified API for 18 live-chat & customer-support widgets.
Zero dependencies. Tree-shakeable. SSR-safe. Strict TypeScript.
Every live-chat vendor ships its own snippet, its own globals, its own
quirks. Wrappers exist for individual ones (react-use-intercom,
react-zendesk, tawk-messenger-react, …) — but each pulls in a
framework, locks you to one provider, and re-introduces the bugs the
underlying snippet already had: SSR crashes, calls dropped before boot,
HMAC fields silently missing, no shutdown→boot reversibility.
ahize is one zero-dependency layer over all of them. Same surface,
swap providers by changing the import path. Pre-boot calls are
buffered. Identity verification is typed. SSR is a no-op. CSP is
documented per provider.
Today you’re on Intercom:
import * as chat from "ahize/intercom"
await chat.load({ appId: "abc" })
chat.identify({ id: "u1", email: "ada@example.com" })
chat.track("plan_upgraded", { tier: "pro" })
chat.show()
Tomorrow you switch to Crisp — only the import path and the load()
options change:
import * as chat from "ahize/crisp"
await chat.load({ websiteId: "uuid" })
chat.identify({ id: "u1", email: "ada@example.com" })
chat.track("plan_upgraded", { tier: "pro" })
chat.show()
npm install ahize
# pnpm add ahize / yarn add ahize / bun add ahize
import { load, identify, track, show } from "ahize/intercom"
await load({ appId: "abc123" })
await identify({
id: "user_1",
email: "ada@example.com",
name: "Ada Lovelace",
})
await track("plan_upgraded", { tier: "pro" })
await show()
That’s it. The Intercom CDN is injected, boot is fired, and your
identify/track/show calls were buffered and drained in order.
Every provider exports the exact same functions:
load(options); // inject CDN & boot — Promise resolves when widget API attaches
identify(identity); // set user; verification: hmac | jwt | callback
track(event, metadata?); // emit a custom event; generic <T extends EventMetadata>
pageView({ path, locale }); // notify on SPA route change
show();
hide(); // toggle widget visibility
shutdown(); // end session — keeps config so you can re-identify
destroy(); // hard reset — removes script, globals, listeners
ready(); // Promise<void> resolved once the real API is live
isReady();
state(); // synchronous lifecycle ('idle'|'loading'|'ready'|'shutdown')
getIdentity();
onIdentityChange(cb); // typed identity transitions
If you call any method before load() resolves, the call is queued and
flushed in order once the provider is ready. No more “Intercom is not
defined” warnings.
On top of the unified surface, most providers expose a typed
on(event, handler) for their documented lifecycle events (widget
open/close, message sent/received, conversation started, unread count,
…) and a handful of vendor-native methods — see the Providers
table for the per-provider extras.
Most providers want a server-issued HMAC or JWT to prevent users from
spoofing each other’s profiles. ahize makes it a typed required field:
// Intercom — HMAC of user_id with your app secret
await identify({
id: "user_1",
email: "ada@example.com",
verification: { kind: "hmac", hash: process.env.INTERCOM_USER_HASH! },
})
// HubSpot — JWT
await identify({
id: "user_1",
email: "ada@example.com",
verification: { kind: "jwt", token: serverIssuedJwt },
})
// Zendesk Messenger — callback for token refresh on 401
await identify({
id: "user_1",
verification: {
kind: "callback",
getToken: async () => fetchFreshJwtFromServer(),
},
})
Pass the wrong kind for a provider and you get a typed rejection — no
silent drop. See ahize/capabilities for who supports what.
ahize is safe to import from any server runtime. Every method
short-circuits when window/document are unavailable, so this works
in Next.js App Router, Nuxt 4, Remix, SvelteKit, Astro, and Cloudflare
Workers without guards.
// app/layout.tsx — no "use client" needed for the import itself
import { load } from "ahize/intercom"
await load({ appId: "..." }) // resolves to undefined on the server, no-op
If you want to be belt-and-braces sure no DOM code enters your SSR
bundle, import the matching no-op stub:
import { load, identify, track } from "ahize/server"
Each adapter is framework-agnostic — bring your own React/Vue/Angular,
no peer dependencies.
"use client"
import * as React from "react"
import * as intercom from "ahize/intercom"
import { createAhizeComponent } from "ahize/next"
import { usePathname, useSearchParams } from "next/navigation"
const Ahize = createAhizeComponent(React, { usePathname, useSearchParams })
export default function ChatBoot() {
return <Ahize provider={intercom} options={{ appId: "abc123" }} autoPageView />
}
Mount once in your root layout. pageView() auto-fires on every route
change — fixes HubSpot’s targeting rules, keeps Intercom’s session
tracking accurate.
// app/plugins/ahize.client.ts (Nuxt 4 default srcDir)
// plugins/ahize.client.ts (Nuxt 3, or Nuxt 4 with custom srcDir)
import { defineNuxtPlugin } from "#app"
import * as intercom from "ahize/intercom"
import { createNuxtAhizePlugin } from "ahize/nuxt"
export default defineNuxtPlugin(
createNuxtAhizePlugin({
provider: intercom,
options: { appId: "abc123" },
autoPageView: true,
}),
)
The plugin & defineNuxtPlugin API is identical between Nuxt 3 and 4 —
only the default source directory changed (app/ in Nuxt 4). Use
$ahize from useNuxtApp() to access the provider in components.
import * as React from "react"
import * as crisp from "ahize/crisp"
import { createUseAhize } from "ahize/react"
const useAhize = createUseAhize(React)
function App() {
const { isReady, identify, show } = useAhize({
provider: crisp,
options: { websiteId: "..." },
})
return (
<button disabled={!isReady} onClick={() => show()}>
Open chat
</button>
)
}
All shipped — see ahize/vue, ahize/svelte, ahize/sveltekit,
ahize/remix, ahize/astro, ahize/angular. Same factory pattern:
pass the framework primitives in.
load() never fires on import. Pair it with your CMP:
import * as intercom from "ahize/intercom"
OneTrust.OnConsentChanged(() => {
if (OnetrustActiveGroups.includes("C0004")) {
intercom.load({ appId: "abc123" })
} else {
intercom.destroy() // removes script, globals, cookies
}
})
Defer strategies for LCP-sensitive pages:
load({ appId: "...", defer: "idle" }) // requestIdleCallback
load({ appId: "...", defer: "interaction" }) // first pointerdown/scroll/keydown
load({ appId: "...", defer: "manual" }) // never auto-injects
load({ appId: "...", consent: hasConsent }) // gate behind a flag
EU region selection is built in:
intercom.load({ appId: "...", region: "eu" }) // → api-iam.eu.intercom.io
hubspot.load({ portalId: "...", region: "eu1" }) // → js-eu1.hs-scripts.com
Strict CSP breaks every chat widget by default. ahize/csp ships the
exact directive list per provider, including the WSS endpoints
competitor wrappers always forget:
import { cspDirectives, mergeCsp, toHeaderString } from "ahize/csp"
const policy = mergeCsp(
cspDirectives("intercom"),
cspDirectives("crisp"),
cspDirectives("chatwoot", { chatwootBaseUrl: "https://chat.acme.com" }),
)
response.setHeader("Content-Security-Policy", toHeaderString(policy))
Pass a nonce through load() and the same nonce through your CSP:
load({ appId: "...", nonce: cspNonce })
Want to catch violations in dev?
import { watchCspViolations } from "ahize/csp"
watchCspViolations((event) => {
console.warn("CSP blocked", event.blockedURI, "for", event.violatedDirective)
})
For pages where chat is below the fold and LCP matters, mount a tiny
launcher. The real provider boots on hover or click:
import * as intercom from "ahize/intercom"
import { mountFacade } from "ahize/facade"
mountFacade({
provider: "intercom",
boot: () => intercom.load({ appId: "abc123" }),
})
Under 2 KB. No CSS file, no framework. The launcher removes itself once
the real widget is ready.
Capability matrix is queryable so you don’t hard-code branches:
import { capabilities, supports } from "ahize/capabilities"
if (supports("zendesk", "callback")) {
await identify({ id: "u1", verification: { kind: "callback", getToken } })
} else if (supports("zendesk", "jwt")) {
await identify({ id: "u1", verification: { kind: "jwt", token } })
}
When the snippet refuses to load, ahize/diagnostics probes the CDN and
returns a hint:
import { diagnose } from "ahize/diagnostics"
const result = await diagnose("intercom", { appId: "abc123" })
// { ok: false, status: 404, hint: "Snippet not found — typo in id…" }
Every provider ships the unified surface (load / identify / track /
pageView / show / hide / shutdown / destroy / ready / isReady /
state / getIdentity / onIdentityChange) plus a provider-specific
extension: on(event, handler) typed event bridge, and vendor-native
methods where they exist. Audited against live vendor docs 2026-04-16.
| Provider | Sub-path | Identity / regions | Provider-specific extras |
|---|---|---|---|
| Intercom | ahize/intercom |
HMAC, JWT, us/eu/au |
showSpace, showNewMessage, startTour/Survey/Checklist, showArticle/News/Ticket, getVisitorId, onShow/onHide/onUserEmailSupplied, 11 typed boot fields |
| Crisp | ahize/crisp |
HMAC, hot-reconfigure | open/close/toggle, sendMessage, helpdeskSearch, setSessionSegments, setUserAvatar, 13 events, runtime config |
| Tawk.to | ahize/tawk |
HMAC, login() restores history |
maximize/minimize/popup, addTags/removeTags, getStatus/isChat*, visitor preload, 19 event hooks |
| Zendesk | ahize/zendesk |
JWT, callback | open/close, setConversationTags, setCustomization, newConversation, resetWidget, 13 messenger:on events, cookies/zIndex config |
| Chatwoot | ahize/chatwoot |
HMAC, self-hosted, settings | setColorScheme, deleteAttribute, popoutChatWindow, setLocale, setBubbleVisibility, on(opened/closed/postback/…), 11 typed settings |
| HubSpot | ahize/hubspot |
Identification token, na1/eu1/ap1 |
on(conversationStarted/…) (8 events), status(), refresh, 7 typed config (cookie banner, inline embed, attachment, CSP) |
| LiveChat | ahize/livechat |
— | maximize(draft?), minimize, hideGreeting, triggerSalesTracker, getState/CustomerData/ChatData, 10 events, 7 typed __lc fields |
| Freshchat | ahize/freshchat |
JWT, us/eu/in/au |
open/close, setLocale, setTags/setFaqTags, setBotVariables, trackPage, isOpen/isLoaded, 16 events, 8 typed init fields |
| Olark | ahize/olark |
— | getVisitorDetails(), sendMessage/NotificationTo*, setOperatorGroup, setLocale, 12 events, group/locale boot config |
| HelpScout | ahize/helpscout |
HMAC | search, article, sessionData, config, reset, toggle, askQuestion, showMessage, info, once, prefill(attachments), full BeaconConfig object |
| LiveAgent | ahize/liveagent |
self-hosted opt | addUserDetail, addTicketField, setVisitorLocation, createForm, hasOpenedWidget, on(chatStarted/chatEnded/online/offline) |
| Gist | ahize/gist |
HMAC | open/close, showLauncher/hideLauncher, navigate, showArticle, trigger, setSidebar/setStandard, 12 events |
| JivoChat | ahize/jivochat |
setUserToken (verification) |
setClientAttributes (rate-limited), setCustomData, startCall, sendOfflineMessage, sendPageTitle, 12 events, sync chatMode/getUnreadMessagesCount/getUtm |
| Smartsupp | ahize/smartsupp |
— | open/close, prefillMessage, sendMessage, setGroup, setLanguage, getVisitorId, on(messageSent/Received/messengerClose), 12 typed _smartsupp fields |
| Tidio | ahize/tidio |
— | tidioChatApi.track forwarding, setColorPalette, display, messageFromOperator/Visitor, addVisitorTags, setVisitorCurrency, 10 events, pre-load language/identify |
Still functional but the underlying vendor has announced sunset / EOL.
Wrappers emit a one-shot console.warn on first load() and are marked
@deprecated in JSDoc. No new feature work planned.
| Provider | Sub-path | Status |
|---|---|---|
| Drift | ahize/drift |
Vendor sunset announced 2026-03-06 (Clari + Salesloft). |
| Sendbird | ahize/sendbird |
AI Chatbot Widget discontinued; repo archived 2025-07-09 at v1.9.7. Consider Sendbird Desk. |
| Userlike | ahize/userlike |
v1 CDN EOL 2026-08-01. Vendor rebranded to Lime Connect; v2 is @userlike/messenger with a different surface. |
| Zendesk Classic | ahize/zendesk-classic |
Limited to Zendesk accounts created before 2023-06-05. Chat Web SDK removal started 2025-04-30. Use ahize/zendesk (Messenger). |
From react-use-intercom:
-import { IntercomProvider, useIntercom } from "react-use-intercom";
+import * as intercom from "ahize/intercom";
+import { createUseAhize } from "ahize/react";
+import * as React from "react";
+const useAhize = createUseAhize(React);
-<IntercomProvider appId="abc">{children}</IntercomProvider>
+const { isReady, identify, show, hide, shutdown } = useAhize({
+ provider: intercom,
+ options: { appId: "abc" },
+});
| react-use-intercom | ahize |
|---|---|
boot(props) |
load({ appId, ...props }) |
update(props) |
identify(props) |
trackEvent(name, meta) |
track(name, meta) |
shutdown() |
shutdown() |
hardShutdown() |
destroy() |
boot({ user_hash }) |
identify({ verification: { kind: "hmac", hash } }) |
boot({ intercom_user_jwt }) |
identify({ verification: { kind: "jwt", token } }) |
Same shape works for react-zendesk, tawk-messenger-react,
@productdevbook/chatwoot, @livechat/widget-react. The notable change
across all of them: ahize separates boot (load) from user
identity (identify).
A plain Vite + TypeScript playground is checked into playground/ for
trying a real widget in the browser without setting up a framework.
pnpm playground
That installs the playground’s own deps (vite, typescript) and opens the
dev server on http://localhost:5173. It imports the Chatwoot provider
directly from ../src/providers/chatwoot.ts, so any code change in
src/ reloads instantly — no build step. Paste your websiteToken (and
baseUrl if self-hosted), hit load(), then play with
identify/track/show/hide/setLocale and watch the event log.
If ahize saves you a few hours of live-chat integration pain, consider
sponsoring on GitHub so
work on the next provider + framework adapter keeps going.
Issues and PRs welcome at
github.com/productdevbook/ahize.
Missing a provider? The pattern is small enough to copy from any
existing one — src/providers/livechat.ts is a good minimal template.
ahize exists because every one of these libraries solved part of the
problem and showed us the bugs to design around. Many of the design
decisions in ahize are direct responses to issues filed on these
projects — thank you to every maintainer and reporter.
undefinedIntercomundefined
devrnt/react-use-intercom — the gold standard React wrapper; informed our queue-before-load contract and shutdown reversibility designnhagen/react-intercom — early prior art for SSR-safe injection@intercom/messenger-js-sdk — Intercom’s own typed helperundefinedCrispundefined
crisp-im/crisp-sdk-web — official wrapper; our $crisp.push-only contract comes from issues filed against itundefinedTawk.toundefined
tawk/tawk-messenger-react and tawk-messenger-vue-3 — typing gaps & switchWidget bug shaped our typed surfaceundefinedZendeskundefined
B3nnyL/react-zendeskdansmaculotte/vue-zendesk and nuxt-zendesk — defer & GDPR prior artmultivoltage/react-use-zendesk — JWT login flow patternsundefinedHubSpotundefined
adamsoffer/react-hubspotaaronhayes/react-use-hubspot-form — EU region request that drove our region selectorundefinedChatwootundefined
chatwoot/chatwoot — the upstream widget SDK and every issue/PR against its window-event API@productdevbook/chatwoot — earlier work that informed ahize/chatwoot’s shapeundefinedPerformance & deferred loadundefined
calibreapp/react-live-chat-loader — the facade pattern, ported here as ahize/facade@builder.io/partytown — worker offload, wired through ahize/partytownundefinedLiveChat (text.com)undefined
livechat/chat-widget-adapters — official multi-framework reference (@livechat/widget-react etc.)undefinedOther providersundefined
userlike/messenger — Result<ok, err> pattern we propagateundefinedOther unified-chat workundefined
@dannyfranca/any-chat — earlier multi-provider experimentIf your library should be on this list and isn’t,
open an issue — happy
to credit.
We use cookies
We use cookies to analyze traffic and improve your experience. You can accept or reject analytics cookies.