Install
Paste this just before </body> on every page. Your workspace ID is already filled in on Integrations → Website chat in Frictionless, which everyone on your team can open.
<script src="https://usefrictionless.com/widget/v1.js"></script>
<script>
Frictionless.init({ company_id: "YOUR_WORKSPACE_ID" });
</script>That’s all you need for live chat. Everything below is optional.
Check it worked
- Open Integrations → Website chat in Frictionless and leave it open. The Install tab checks by itself and turns green within seconds of your site loading the chat.
- On
localhostit says Testing. It turns green once a real website loads it. - Nothing? Open your browser’s console. A wrong workspace ID shows as an error starting with
[Frictionless].
Next.js
Don’t paste the raw tags into a component: init() can run before the script has loaded. Use next/script and put the component in your root layout, which stays mounted as visitors move between pages.
// app/frictionless-chat.tsx
"use client";
import Script from "next/script";
export default function FrictionlessChat() {
return (
<Script
src="https://usefrictionless.com/widget/v1.js"
strategy="afterInteractive"
onReady={() => window.Frictionless.init({ company_id: process.env.NEXT_PUBLIC_FRICTIONLESS_WORKSPACE_ID! })}
/>
);
}// app/layout.tsx
import FrictionlessChat from "./frictionless-chat";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<FrictionlessChat />
</body>
</html>
);
}Calling init() twice is safe: the second call is ignored, so a remount never adds a second chat.
React (Vite and others)
import { useEffect } from "react";
export function useFrictionless(companyId: string) {
useEffect(() => {
// Queued until the script arrives; a second init() is ignored
const f = ((window as any).Frictionless ||= { q: [] });
f.init ||= (...args: unknown[]) => f.q.push(["init", args]);
f.init({ company_id: companyId });
if (document.getElementById("frictionless-chat")) return;
const script = document.createElement("script");
script.id = "frictionless-chat";
script.src = "https://usefrictionless.com/widget/v1.js";
script.async = true;
document.body.appendChild(script);
}, [companyId]);
}TypeScript
Types for window.Frictionless are at usefrictionless.com/widget/v1.d.ts. Save the file into your project, for example as types/frictionless.d.ts.
WordPress, Shopify and site builders
- WordPress: install a plugin that adds code to the footer, such as WPCode, and paste the two lines into the footer section.
- Shopify: Online Store → Themes → ⋯ → Edit code →
theme.liquid. Paste just before</body>and save. - Webflow, Wix, Squarespace: paste into the site-wide custom code setting for the footer, or end of body.
Loading it async
With async or defer, the script may arrive after your own code runs. This version queues calls until it does, so init(), identify() and track() are safe to call at any time:
<script>
window.Frictionless = window.Frictionless || { q: [] };
["init", "identify", "track", "setContext", "open", "reset"].forEach(function (m) {
Frictionless[m] = Frictionless[m] || function () { Frictionless.q.push([m, arguments]); };
});
Frictionless.init({ company_id: "YOUR_WORKSPACE_ID" });
</script>
<script async src="https://usefrictionless.com/widget/v1.js"></script>Signed-in users and signing out
To show agents who a visitor is, call identify() with a userHash computed on your server. The Context API guide shows how.
When a user signs out, call Frictionless.reset(). It forgets the visitor on that browser, so the next person to use it starts a new chat and can’t see the last one.
async function signOut() {
await yourSignOut();
window.Frictionless?.reset();
}Single-page apps
Load the chat once, in a layout that stays mounted. It keeps its conversation and open state as visitors move between pages, with or without a full page load. To tell agents where the visitor is, call track() when the route changes, for example track("Viewed pricing").
Your own button
Frictionless.init({ company_id: "YOUR_WORKSPACE_ID", launcher: false });
myButton.onclick = () => Frictionless.toggle();
Frictionless.on("unread", (count) => { myBadge.textContent = count || ""; });Also: open(), close(), open("help"), showLauncher(), hideLauncher(), and position: "left" in init(). The chat starts closed, as a button, until a visitor opens it.
Allowed websites
Your workspace ID is public, so by default any website can show your chat. To stop that, add your websites in Integrations → Website chat → Install. A website also covers its subdomains, so example.com allows shop.example.com. Your chat link and localhost always work, so you can keep testing.
On any other website the chat doesn’t appear, and the browser console explains why. This stops other websites from embedding your chat. It isn’t a login, so keep private data out of track().
Content Security Policy
If your site sets a Content Security Policy, allow:
script-src usefrictionless.com
connect-src api.usefrictionless.com wss://api.usefrictionless.com
img-src api.usefrictionless.com blob: https:
media-src api.usefrictionless.com blob:
style-src 'unsafe-inline'img-src https: covers pictures in your News posts, which can come from any address. style-src 'unsafe-inline' is needed because the chat’s styles live inside its own Shadow DOM, where your site’s CSS can’t reach them.