JavaScript SDK
The @yak-io/javascript package provides the core client SDK and server handlers. It works with any JavaScript runtime that supports the Fetch API.
When to Use
- Building with frameworks without a dedicated SDK (Solid, Ember, etc.)
- Need server handlers for custom runtimes (Cloudflare Workers, Deno, Bun)
- Building a custom integration without framework components
Installation
npm install @yak-io/javascriptpnpm add @yak-io/javascriptyarn add @yak-io/javascriptbun add @yak-io/javascriptShips both ESM and CommonJS, so it works with native ESM, bundlers, and CommonJS
require() / Jest out of the box — no transform config needed. See
Module Format Issues if you
hit an Unexpected token 'export' error on older tooling.
Server Handlers
Create API endpoints that serve route configuration and handle tool calls:
import { createYakHandler } from "@yak-io/javascript/server";
const { GET, POST } = createYakHandler({
routes: [
{ path: "/", title: "Home" },
{ path: "/pricing", title: "Pricing" },
],
tools: [
{
id: "custom",
getTools: async () => [
{
name: "greet",
description: "Greet a user by name",
inputSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
},
},
],
// `args` arrives as `unknown` — narrow it before use.
executeTool: async (name, args) => {
if (name === "greet") {
const { name: who } = args as { name: string };
return { message: `Hello, ${who}!` };
}
throw new Error(`Unknown tool: ${name}`);
},
},
],
});
export { GET, POST };Splitting config and tools across routes
createYakHandler returns a GET (config) and a POST (tool execution) for one endpoint. If your runtime needs them on separate routes, build each half on its own:
import {
createYakConfigHandler,
createYakToolsHandler,
} from "@yak-io/javascript/server";
// GET — serves routes + the tool manifest
export const GET = createYakConfigHandler({ routes, tools });
// POST — executes a tool call
export const POST = createYakToolsHandler({ tools });Each returns a single (req: Request) => Promise<Response>. createYakConfigHandler needs routes (with tools optional); createYakToolsHandler needs at least one tool source and throws at construction if given none.
Route Schema
| Property | Type | Required | Description |
|---|---|---|---|
path | string | Yes | URL path |
title | string | No | Human-readable title |
description | string | No | Brief description for AI context |
search | RouteSearch | No | Marks the route as able to run a free-text search, e.g. { queryParam: "q" } lets "show me boots" drive /search?q=boots. The assistant prefers navigating here over answering inside the chat. |
filters | RouteFilter[] | No | Query-param filters this route accepts. The assistant only sets params you declare here and never invents others, so "size 10 boots" can reliably drive /products?size=10. |
See Manual Routes for worked search and filters examples.
Dynamic Route Sources
Fetch routes from external sources:
const staticRoutes = [
{ path: "/", title: "Home" },
{ path: "/products", title: "Products" },
];
const cmsRoutes = {
id: "cms",
getRoutes: async () => {
const res = await fetch("https://cms.example.com/api/pages");
return res.json();
},
};
const { GET, POST } = createYakHandler({
routes: [staticRoutes, cmsRoutes],
});Drop-in widget (YakEmbed)
For vanilla JS apps, YakEmbed renders the trigger pill and manages the chat iframe and voice session for you:
import { YakEmbed } from "@yak-io/javascript";
const embed = new YakEmbed({
appId: "your-app-id",
mode: "both", // "chat" (default) | "voice" | "both"
getConfig: async () => fetch("/api/yak").then((r) => r.json()),
onToolCall: async (name, args) => {
const res = await fetch("/api/yak", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, args }),
});
const data = await res.json();
if (!data.ok) throw new Error(data.error);
return data.result;
},
});
embed.mount();The same onToolCall and getConfig handlers serve chat and voice — see Voice Mode for the full voice API exposed via embed.voiceStart() / voiceStop() / voiceToggle().
Client SDK
The YakClient class manages widget communication. For most use cases, use @yak-io/react or @yak-io/nextjs instead — this is the low-level API.
YakClient is headless. It does not render anything: mount() only starts listening for messages from the widget iframe. Use it only when you render and own the iframe yourself — you must hand its contentWindow to setIframeWindow() before sendPrompt() or setWidgetOpen() will do anything. If you just want a working widget, use YakEmbed above, which creates the iframe for you.
import { YakClient } from "@yak-io/javascript";
const client = new YakClient({
appId: "your-app-id",
// YakClient has no `getConfig` — pass routes and tools directly.
chatConfig: { routes, tools },
onToolCall: async (name, args) => {
const res = await fetch("/api/yak", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, args }),
});
const data = await res.json();
if (!data.ok) throw new Error(data.error);
return data.result;
},
onReady: () => console.log("Widget ready"),
onClose: () => console.log("Widget closed"),
});
client.mount();
// Render the iframe yourself, then hand its window to the client.
const iframe = document.createElement("iframe");
iframe.src = client.getEmbedUrl();
iframe.addEventListener("load", () => {
client.setIframeWindow(iframe.contentWindow);
client.setWidgetOpen(true);
client.sendPrompt("Help me");
});
document.body.appendChild(iframe);
// On teardown:
// client.setIframeWindow(null);
// client.unmount();Configuration Options
| Option | Type | Description |
|---|---|---|
appId | string | Your Yak application ID |
chatConfig | ChatConfig | Routes and tools to send to the widget. YakClient has no getConfig — this is how you supply them. |
onToolCall | (name, args) => Promise<unknown> | Tool execution handler |
onToolCallComplete | (event: ToolCallEvent) => void | Called after each tool call completes |
onRedirect | (path: string) => void | Navigation handler |
onReady | () => void | Called when the widget is ready |
onClose | () => void | Called when the widget is closed |
theme | Theme | Widget styling options |
origin | string | Override the chat origin (defaults to https://chat.yak.io). Most integrators never set this — it exists for non-production environments. |
options.disableRestartButton | boolean | Hide the restart session button in the widget header. |
options.disablePageContent | boolean | Stop sending any page context (URL, title, and visible text) to the assistant. The widget still works, but it won't be aware of the page the user is on. |
user | { id, hash } | Signed end-user identity. Enables conversation persistence and history. |
Privacy: By default Yak shares the current page's URL, title, and visible text with the assistant so it can answer questions about the page the user is viewing. Set options.disablePageContent to turn this off entirely — the SDK then sends nothing about the page (not even the URL), so the assistant can't answer page-specific questions.
Runtime Examples
Cloudflare Workers
import { createYakHandler } from "@yak-io/javascript/server";
const { GET, POST } = createYakHandler({
routes: [{ path: "/", title: "Home" }],
});
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/yak") {
if (request.method === "GET") return GET(request);
if (request.method === "POST") return POST(request);
}
return new Response("Not Found", { status: 404 });
},
};Hono
import { Hono } from "hono";
import { createYakHandler } from "@yak-io/javascript/server";
const app = new Hono();
const { GET, POST } = createYakHandler({
routes: [{ path: "/", title: "Home" }],
});
app.get("/api/yak", (c) => GET(c.req.raw));
app.post("/api/yak", (c) => POST(c.req.raw));
export default app;Deno
import { createYakHandler } from "@yak-io/javascript/server";
const { GET, POST } = createYakHandler({
routes: [{ path: "/", title: "Home" }],
});
Deno.serve(async (request) => {
const url = new URL(request.url);
if (url.pathname === "/api/yak") {
if (request.method === "GET") return GET(request);
if (request.method === "POST") return POST(request);
}
return new Response("Not Found", { status: 404 });
});Bun
import { createYakHandler } from "@yak-io/javascript/server";
const { GET, POST } = createYakHandler({
routes: [{ path: "/", title: "Home" }],
});
Bun.serve({
port: 3000,
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/api/yak") {
if (request.method === "GET") return GET(request);
if (request.method === "POST") return POST(request);
}
return new Response("Not Found", { status: 404 });
},
});Express / Node.js
import express from "express";
import { createYakHandler } from "@yak-io/javascript/server";
const app = express();
app.use(express.json());
const { GET, POST } = createYakHandler({
routes: [{ path: "/", title: "Home" }],
});
app.all("/api/yak", async (req, res) => {
const url = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
const request = new Request(url, {
method: req.method,
headers: req.headers as HeadersInit,
body: req.method !== "GET" ? JSON.stringify(req.body) : undefined,
});
const response = await (req.method === "POST" ? POST(request) : GET(request));
res.status(response.status);
response.headers.forEach((value, key) => res.setHeader(key, value));
res.send(Buffer.from(await response.arrayBuffer()));
});
app.listen(3000);TypeScript
Import types for your integrations:
// Client-side types
import type {
YakClientConfig,
Theme,
ThemeColors,
ToolCallHandler,
ToolCallEvent,
} from "@yak-io/javascript";
// Server-side types
import type {
RouteInfo,
RouteSource,
ToolDefinition,
ToolSource,
ToolExecutor,
ToolCallPayload,
ToolCallResult,
} from "@yak-io/javascript/server";