Getting started
Installation
Script tag, ESM import, React, Next.js, Vue, Angular and WebView notes.
There are three ways to get the widget onto a page. They all create the same <hive-chat> element and talk to the same API — pick whichever fits your build:
| Method | Best for | apiUrl |
|---|---|---|
Script tag (/embed/v1/hive-embed.js) | CMSs, marketing sites, tag managers, anything without a bundler. | Inferred from the script’s origin. |
ESM / npm (@hive/embed) | Bundled apps (Vite, webpack, esbuild) in any framework. | Pass apiUrl (or call setDefaultApiUrl). |
React wrapper (@hive/embed/react) | React 18/19 and Next.js apps. | Pass apiUrl as a prop. |
Side by side
The same widget in every stack. Replace the key and origin with the values from your console.
<script async
src="https://console.agenticworkforce.me/embed/v1/hive-embed.js"
data-key="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"></script>import { HiveChat } from '@hive/embed';
const chat = HiveChat.init({
key: 'hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY',
apiUrl: 'https://console.agenticworkforce.me',
});
// chat.open(); chat.send('Hello'); chat.destroy();import { HiveChat } from '@hive/embed/react';
export function SupportChat() {
return (
<HiveChat
embedKey="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"
apiUrl="https://console.agenticworkforce.me"
onMessage={(m) => console.log(m.role, m.text)}
/>
);
}// app/components/support-chat.tsx
'use client';
import dynamic from 'next/dynamic';
// @hive/embed registers <hive-chat> (extends HTMLElement) at import time, so it
// must never be evaluated on the server. Client components are still
// server-rendered: import the wrapper only in the browser with ssr: false.
// (next/dynamic with ssr: false is only allowed inside a client component.)
const HiveChat = dynamic(
() => import('@hive/embed/react').then((m) => m.HiveChat),
{ ssr: false },
);
export function SupportChat() {
return (
<HiveChat
embedKey="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"
apiUrl="https://console.agenticworkforce.me"
/>
);
}
// app/layout.tsx (Server Component) — render it once so it survives navigation:
// import { SupportChat } from './components/support-chat';
// … <body>{children}<SupportChat /></body><script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue';
import { HiveChat, type HiveChatHandle } from '@hive/embed';
let chat: HiveChatHandle | undefined;
onMounted(() => {
chat = HiveChat.init({
key: 'hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY',
apiUrl: 'https://console.agenticworkforce.me',
});
});
onBeforeUnmount(() => chat?.destroy());
</script>
<template>
<!-- Launcher mode mounts to <body>; nothing to render here. -->
<button @click="chat?.open()">Chat with us</button>
</template>// support-chat.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { HiveChat, type HiveChatHandle } from '@hive/embed';
@Component({
selector: 'app-support-chat',
standalone: true,
template: '<button (click)="open()">Chat with us</button>',
})
export class SupportChatComponent implements OnInit, OnDestroy {
private chat?: HiveChatHandle;
ngOnInit(): void {
this.chat = HiveChat.init({
key: 'hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY',
apiUrl: 'https://console.agenticworkforce.me',
});
}
open(): void {
this.chat?.open();
}
ngOnDestroy(): void {
this.chat?.destroy();
}
}
// If you use <hive-chat> directly in a template instead, add
// schemas: [CUSTOM_ELEMENTS_SCHEMA] to the component and import '@hive/embed' once.Script tag
The loader is an IIFE. With data-key present it initializes on DOMContentLoaded (or immediately if the body already exists). Add async: the loader finds its own tag by URL even when document.currentScript is null.
Manual init and the pre-load queue
Use data-manual (or omit data-key) and call window.HiveChat.init(options) yourself. If your code may run before the loader has arrived, push into a queue — the loader drains it on boot:
<script async src="https://console.agenticworkforce.me/embed/v1/hive-embed.js" data-manual></script>
<script>
// Queue pattern: safe to call before the loader has finished downloading.
window.HiveChat = window.HiveChat || { q: [] };
window.HiveChat.q.push([{
key: 'hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY',
locale: 'auto',
welcome: 'Hi! Ask me anything about our products.',
}]);
</script>Inline mode in a container
data-mode="inline" renders the panel into the element matched by data-container (a CSS selector) instead of a floating launcher. The panel fills its container — give the container a height.
<div id="support" style="height: 600px"></div>
<script async src="https://console.agenticworkforce.me/embed/v1/hive-embed.js"
data-key="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"
data-mode="inline"
data-container="#support"></script>All data-* attributes are listed in the configuration reference.
ESM import of the Web Component
import '@hive/embed' registers the hive-chat element (idempotent). You can then either create elements in markup or call HiveChat.init. Elements need to know the API origin: pass api-url per element or set a default once.
import '@hive/embed'; // side-effect import registers <hive-chat>
import { setDefaultApiUrl } from '@hive/embed';
// Optional: set once so <hive-chat> elements without api-url know the origin.
setDefaultApiUrl('https://console.agenticworkforce.me');
document.body.insertAdjacentHTML(
'beforeend',
'<hive-chat key="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"></hive-chat>',
);Without a bundler you can import the hosted ESM build directly from the platform origin:
<script type="module">
import 'https://console.agenticworkforce.me/embed/v1/hive-embed.mjs'; // registers <hive-chat>
</script>
<hive-chat
key="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"
api-url="https://console.agenticworkforce.me"
locale="ar"
mode="inline"
theme='{"primary":"#0f766e","radius":14}'
style="display:block;height:600px"></hive-chat>React
@hive/embed/react exports a HiveChat component. React is a peer dependency; the wrapper pushes props through element.configure() so objects survive intact, and maps callbacks to the host events. The key prop is named embedKey because key is reserved by React.
import { HiveChat } from '@hive/embed/react';
export function SupportChat() {
return (
<HiveChat
embedKey="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"
apiUrl="https://console.agenticworkforce.me"
onMessage={(m) => console.log(m.role, m.text)}
/>
);
}| Prop | Signature | Description |
|---|---|---|
embedKey | embedKey: string | The publishable key (named embedKey because key is reserved by React). |
className | className?: string | Class applied to the <hive-chat> host element. |
style | style?: CSSProperties | Inline style on the host (size it in inline mode). |
onReady | onReady?: (info: { key_id }) => void | Maps to hive:ready — config loaded, widget usable, no session yet. |
onSession | onSession?: (info: { visitor_id, identified }) => void | Maps to hive:session — the session was minted (first open / first send). |
onOpen | onOpen?: () => void | Maps to hive:open. |
onClose | onClose?: () => void | Maps to hive:close. |
onMessage | onMessage?: (m: { role: 'user' | 'agent', text }) => void | Maps to hive:message. |
onError | onError?: (e: { code }) => void | Maps to hive:error. |
Plus every HiveChat.init option except key (apiUrl, locale, theme, title, subtitle, welcome, placeholder, user, mode, open, storage, visitorId) — see Configuration.
The wrapper does not forward a ref. To call methods (open(), send()) query the element or keep a handle from HiveChat.init:
import { HiveChat } from '@hive/embed/react';
import type { HiveChatElement } from '@hive/embed';
function openChat() {
document.querySelector<HiveChatElement>('hive-chat')?.open();
}
export function Support() {
return (
<>
<button onClick={openChat}>Need help?</button>
<HiveChat embedKey="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY" apiUrl="https://console.agenticworkforce.me" />
</>
);
}Next.js
@hive/embed registers the <hive-chat> custom element (a class extending HTMLElement) the moment it is imported, so the module must never be evaluated on the server — and in the App Router client components are still server-rendered. Two options, in order of preference:
- Script tag via
next/scriptin the root layout — zero npm dependencies, nothing runs on the server, auto-init fromdata-*. This is what the Next.js example does (withdata-manualand a client component callingwindow.HiveChat.initfromuseEffect):TSX // app/layout.tsx — script-tag variant (no npm dependency) import Script from 'next/script'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <Script src="https://console.agenticworkforce.me/embed/v1/hive-embed.js" data-key="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY" strategy="afterInteractive" /> </body> </html> ); } - React wrapper, imported only in the browser. Put
next/dynamicwith{ ssr: false }inside a'use client'file (Next.js 15 rejectsssr: falsein Server Components such asapp/layout.tsx), and render that file once from the layout. A plainimport { HiveChat } from '@hive/embed/react'at the top of a client component crashes the server render withHTMLElement is not defined.TSX // app/components/support-chat.tsx 'use client'; import dynamic from 'next/dynamic'; // @hive/embed registers <hive-chat> (extends HTMLElement) at import time, so it // must never be evaluated on the server. Client components are still // server-rendered: import the wrapper only in the browser with ssr: false. // (next/dynamic with ssr: false is only allowed inside a client component.) const HiveChat = dynamic( () => import('@hive/embed/react').then((m) => m.HiveChat), { ssr: false }, ); export function SupportChat() { return ( <HiveChat embedKey="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY" apiUrl="https://console.agenticworkforce.me" /> ); } // app/layout.tsx (Server Component) — render it once so it survives navigation: // import { SupportChat } from './components/support-chat'; // … <body>{children}<SupportChat /></body>
If your app sends a Content-Security-Policy header, allow the platform origin:
// next.config.ts — if you send a CSP, allow the platform origin
const csp = [
"default-src 'self'",
"script-src 'self' https://console.agenticworkforce.me",
"connect-src 'self' https://console.agenticworkforce.me",
].join('; ');
export default {
async headers() {
return [{ source: '/(.*)', headers: [{ key: 'Content-Security-Policy', value: csp }] }];
},
};Vue
Either call HiveChat.init from onMounted (see the tabs above) or use the element in a template. In the second case tell the compiler that hive-chat is a custom element so it does not warn about an unknown component.
<!-- App.vue: use the element directly -->
<script setup lang="ts">
import '@hive/embed';
</script>
<template>
<hive-chat
key="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"
api-url="https://console.agenticworkforce.me"
mode="inline"
style="display:block;height:600px"
/>
</template>
<!-- vite.config.ts: tell Vue that hive-chat is a custom element -->
<!--
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'hive-chat' } } })
-->Angular
Import @hive/embed once (for example in main.ts) and add CUSTOM_ELEMENTS_SCHEMA to any component that uses the tag. Or drive it from TypeScript with HiveChat.init (tabs above) — no schema needed then.
// main.ts (or the component file)
import '@hive/embed';
// support.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
selector: 'app-support',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<hive-chat
key="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"
api-url="https://console.agenticworkforce.me"
locale="auto"
style="display:block;height:600px"
mode="inline"></hive-chat>
`,
})
export class SupportComponent {}Native apps and WebViews
The widget is plain web: host a tiny HTML page (below) on one of the key’s allowed origins and load it in a WKWebView / Android WebView / Flutter webview_flutter. Notes:
- The WebView must send an
Originheader, i.e. load the page overhttps://from a real host. Pages loaded fromfile://orabout:blankhave an opaque origin and are rejected unless the key’s allowlist contains the wildcard*. - Use
mode="inline"so the panel fills the WebView; there is no floating launcher to tap in a native shell. - Enable JavaScript and DOM storage (Android:
settings.domStorageEnabled = true) so the visitor id and thread persist. With storage disabled usestorage: "none"and pass a stablevisitorIdyou keep on the device (viaHiveChat.initor thevisitor-idattribute — the loader has nodata-visitor-id). - Identified users: mint the identity JWT on your backend and inject it via
data-user-tokenorHiveChat.init({ user: { token } }). - SSE streaming works in all current WebViews; nothing to configure.
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<style>html,body{margin:0;height:100%}</style>
</head>
<body>
<script async src="https://console.agenticworkforce.me/embed/v1/hive-embed.js"
data-key="hive_pk_REPLACE_WITH_YOUR_PUBLISHABLE_KEY"
data-mode="inline"
data-storage="local"></script>
</body>
</html>