Yak Docs
SDKs

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

For framework-specific SDKs, see Next.js, React, Vue, Svelte, Nuxt, or Angular.

Installation

npm install @yak-io/javascript
pnpm add @yak-io/javascript
yarn add @yak-io/javascript
bun add @yak-io/javascript

Ships 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"],
          },
        },
      ],
      executeTool: async (name, args) => {
        if (name === "greet") {
          return { message: `Hello, ${args.name}!` };
        }
        throw new Error(`Unknown tool: ${name}`);
      },
    },
  ],
});

export { GET, POST };

Route Schema

PropertyTypeRequiredDescription
pathstringYesURL path
titlestringNoHuman-readable title
descriptionstringNoBrief description for AI context

Dynamic Route Sources

Fetch routes from external sources:

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.

import { YakClient } from "@yak-io/javascript";

const client = new YakClient({
  appId: "your-app-id",
  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();
client.setWidgetOpen(true);
client.sendPrompt("Help me");
client.unmount();

Configuration Options

OptionTypeDescription
appIdstringYour Yak application ID
onToolCall(name, args) => Promise<unknown>Tool execution handler
onToolCallComplete(event: ToolCallEvent) => voidCalled after each tool call completes
onRedirect(path: string) => voidNavigation handler
onReady() => voidCalled when the widget is ready
onClose() => voidCalled when the widget is closed
themeThemeWidget styling options
options.disablePageContentbooleanStop 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";

On this page