Chat widget (embed)
Embed the nivq chat experience on any site with one tag — a framework-agnostic <nivq-chat> Web Component, a small token broker so your secret never reaches the browser, theming, and configuration.
You can drop the nivq chat experience onto any website or app with a single tag. The widget is a framework-agnostic Web Component (<nivq-chat>) — it works the same in plain HTML, React, Angular, or Vue — and renders inside a Shadow DOM, so its styles never leak into your page and your CSS never breaks it.
<script type="module" src="https://cdn.jsdelivr.net/npm/nivorbit-chat-widget@1/dist/nivq-chat.js"></script>
<nivq-chat
agent-id="YOUR_AGENT_ID"
api-base-url="https://api.example.com"
token-endpoint="/nivq-token">
</nivq-chat>That token-endpoint is the one piece you host yourself. Here's why.
How auth works — read this first
An nivq API client is an OAuth2 credential pair (clientId + clientSecret) used with the client_credentials grant:
clientId + clientSecret → POST /oauth2/token → short-lived access token (~1h)
access token (Bearer) → POST /v1/agents/{agentId}/conversations (chat)The clientSecret is a long-lived root credential — anyone who reads it can mint tokens and run up your usage. So it must never reach the browser. The widget only ever holds a short-lived access token, which it fetches from a small endpoint on your backend (the "token broker"). The secret stays on your server.
Browser (widget) ──POST──► your /nivq-token ──client_credentials──► NivQ /oauth2/token
▲ │
└──── { access_token } ◄────┘ (the secret is never returned to the browser)Never ship the secret to the browser
Don't put clientSecret in the widget, in client-side JS, or in a public config. Keep it on your backend and hand the widget only the short-lived token.
1. Create an API client
In nivq, create an API client (the screen that issues a clientId + clientSecret). On that key, list every web origin the widget will be embedded on under Allowed origins — e.g. https://app.acme.com — since the widget streams chat straight from the browser to your API (see Allow your embedding origin below).
2. Run a token broker
The broker is one endpoint on your backend. It authenticates your end-user with your own session, exchanges the secret for a token, and returns just { access_token, expires_in }. The widget caches the token and refreshes it before it expires.
// Node / Express
import express from "express";
const app = express();
app.post("/nivq-token", async (req, res) => {
// 1. Authenticate YOUR user (session/cookie/JWT). Reject if not signed in.
// if (!req.user) return res.sendStatus(401);
// 2. Exchange the secret for a short-lived token.
const r = await fetch(`${process.env.NIVQ_API_BASE}/oauth2/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.NIVQ_CLIENT_ID,
client_secret: process.env.NIVQ_CLIENT_SECRET, // stays server-side
}),
});
if (!r.ok) return res.sendStatus(502);
const { access_token, expires_in } = await r.json();
// 3. Return ONLY the token — never the secret.
res.json({ access_token, expires_in });
});// Spring Boot
@RestController
class NivqTokenBroker {
private final RestClient client = RestClient.create();
@Value("${nivq.api-base}") String apiBase;
@Value("${nivq.client-id}") String clientId;
@Value("${nivq.client-secret}") String clientSecret; // stays server-side
@PostMapping("/nivq-token")
Map<String, Object> token(/* inject your authenticated principal here */) {
var form = new LinkedMultiValueMap<String, String>();
form.add("grant_type", "client_credentials");
form.add("client_id", clientId);
form.add("client_secret", clientSecret);
var resp = client.post()
.uri(apiBase + "/oauth2/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(form)
.retrieve()
.body(Map.class);
// Return only the token + expiry — never the secret.
return Map.of("access_token", resp.get("access_token"),
"expires_in", resp.get("expires_in"));
}
}3. Embed the widget
Plain HTML / any site
<script type="module" src="https://cdn.jsdelivr.net/npm/nivorbit-chat-widget@1/dist/nivq-chat.js"></script>
<nivq-chat agent-id="…" api-base-url="https://api.example.com" token-endpoint="/nivq-token"></nivq-chat>React
For bundled apps, install from npm:
npm i nivorbit-chat-widgetimport "nivorbit-chat-widget"; // registers <nivq-chat>; ships its own types
export function Support() {
return (
<nivq-chat
agent-id="…"
api-base-url="https://api.example.com"
token-endpoint="/nivq-token"
/>
);
}The package ships type definitions, so the tag is typed out of the box. On React 19's strict JSX, add a one-line module augmentation if your editor flags it:
declare module "react" {
namespace JSX {
interface IntrinsicElements {
"nivq-chat": any;
}
}
}Angular
// app.module.ts — allow custom elements
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
@NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })
export class AppModule {}<nivq-chat agent-id="…" api-base-url="https://api.example.com" token-endpoint="/nivq-token"></nivq-chat>Configuration
| Attribute | Required | Default | Description |
|---|---|---|---|
agent-id | ✅ | — | The nivq agent to chat with |
token-endpoint | ✅ | — | Your token broker URL |
api-base-url | ✅ | — | nivq chat API base (your deployment) |
mode | fab | fab (floating launcher) or inline | |
target | — | Inline only: CSS selector of the container to mount into | |
position | bottom-right | bottom-right or bottom-left (FAB) | |
theme | auto | light, dark, or auto (follows OS) | |
locale | auto | tr, en, or auto (browser language) | |
primary-color | NivQ brand | Hex (#6d28d9) or HSL triplet (265 70% 50%) | |
accent-color | NivQ brand | Hex or HSL triplet | |
radius | 0.625rem | Corner radius (any CSS length) | |
logo-url | NivQ mark | Replace the brand logo | |
brand-name | NivQ | Header title / logo alt text | |
launcher-label | — | Text next to the FAB icon | |
greeting | — | Custom empty-state heading | |
placeholder | localized | Input placeholder text | |
external-user-id | — | Distinguishes your end-user (analytics / isolation) | |
persist | false | Persist the conversation in localStorage | |
start-open | false | FAB only: open the panel on load |
You can also drive it from JavaScript:
const el = document.querySelector("nivq-chat");
el.configure({ primaryColor: "#6d28d9", brandName: "Acme Assistant", mode: "inline" });Theming
Defaults are the nivq brand. Override the key tokens and the rest derive from them:
<nivq-chat … primary-color="#6d28d9" accent-color="#f59e0b" radius="14px"
logo-url="https://acme.com/logo.svg" brand-name="Acme Assistant"></nivq-chat>Allow your embedding origin
The widget streams chat directly from the browser to your api-base-url (the broker only mints tokens), so your API must allow your site's origin. Set it on the API client: under Allowed origins, list every origin the widget is embedded on — scheme://host[:port], no path or wildcard, e.g. https://app.acme.com. An empty list disables browser embedding for that key (server-to-server only). Changes take effect within a minute.
On-prem
Point api-base-url at your own nivq deployment; the same per-key Allowed origins apply. If you'd rather manage embedding domains centrally, the deployment-wide CORS_ALLOWED_ORIGIN_PATTERNS setting does the same — see Configuration.
Privacy
One API client is shared across all end-users of your integration, and conversation history is scoped to the client — not to an individual person. The widget therefore keeps each session local (in memory, or localStorage when persist is on) and does not load shared history. Use external-user-id to distinguish your end-users on the nivq side.