← Blog

How to Build a Claude-Powered Chatbot Widget

2026-10-31 · 5 min read · SubToAPI Team

How to Build a Claude-Powered Chatbot Widget

A chatbot widget is a small embeddable UI — usually a floating button that expands into a chat panel — that you drop into any website with a <script> tag. Building one powered by Claude means wiring up three pieces: a frontend widget that renders messages and handles streaming, a backend endpoint that talks to Claude so your API key never touches the browser, and a lightweight embed script that lets you (or your customers) install it anywhere with one line of HTML.

This guide walks through that architecture end to end: the widget UI, the server-side proxy, streaming responses token-by-token, and how to package it as a portable embed script. We'll use SubToAPI for the Claude connection since it gives you an application API key and HTTPS endpoint without managing OAuth or token refresh yourself — but the architecture below works with any Claude-compatible backend.

Why you need a backend, not just a frontend

The most common mistake is calling Claude directly from JavaScript in the browser. That means shipping your API key to every visitor, which anyone can extract from the network tab and use on your account. Instead:

  1. The widget sends the user's message to your own backend endpoint (e.g. /api/chat).
  2. Your backend calls Claude with a server-side secret.
  3. Your backend streams the response back to the widget.

This also gives you a place to add rate limiting, logging, system prompts, and auth per customer — none of which should live in client-side code.

Step 1: The backend proxy

A minimal Node/Express endpoint that forwards to Claude via SubToAPI:

app.post('/api/chat', async (req, res) => {
  const { messages } = req.body;

  const response = await fetch('https://api.subtoapi.app/v1/messages', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'claude-sonnet-4',
      max_tokens: 1024,
      system: 'You are a concise, friendly support assistant for Acme.',
      messages
    })
  });

  const data = await response.json();
  res.json(data);
});

This is the non-streaming version — fine for a first pass. See /docs/messages for the full request/response shape.

Step 2: Add streaming for a responsive feel

Chatbot widgets feel noticeably better when text appears progressively instead of all at once after a 3-second wait. Stream from Claude and pipe tokens straight to the client over Server-Sent Events:

app.post('/api/chat/stream', async (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');

  const upstream = await fetch('https://api.subtoapi.app/v1/messages', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'claude-sonnet-4',
      max_tokens: 1024,
      stream: true,
      messages: req.body.messages
    })
  });

  for await (const chunk of upstream.body) {
    res.write(chunk);
  }
  res.end();
});

Details on event types and parsing deltas are in /docs/streaming.

Step 3: The widget frontend

The widget itself is just a chat UI that posts to your backend and renders the stream. Key pieces:

async function sendMessage(text) {
  appendMessage('user', text);
  const assistantEl = appendMessage('assistant', '');

  const res = await fetch('/api/chat/stream', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ messages: conversation })
  });

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const delta = parseSSEDelta(buffer); // extract text_delta chunks
    assistantEl.textContent += delta;
  }
}

Keep the widget's DOM footprint small and scoped — use a shadow DOM or a unique prefix on all class names so it doesn't collide with the host site's CSS.

Step 4: Package it as an embed script

To let anyone install the widget with one line, bundle the UI, CSS, and fetch logic into a single JS file that self-mounts:

<script src="https://yourdomain.com/widget.js" data-site-id="acme-123" defer></script>

Inside widget.js, read data-site-id from the script tag, inject a container div, and mount the chat UI into it. The site-id lets your backend apply per-customer system prompts, rate limits, or branding without separate deploys.

Step 5: Add conversation memory and guardrails

Two things turn a demo into a usable product:

If you need the widget to take actions — look up an order, check a status — rather than just answer questions, add tool calls on the backend. See /docs/tools for the request format.

Keeping it production-ready

Full request examples and parameter reference live in /docs/quickstart and /pricing if you want to estimate cost before launch.

Questions

Do I need a backend, or can the widget call Claude directly from the browser? You need a backend. Calling Claude directly from client-side JavaScript exposes your API key to anyone who opens the browser's network tab, which is a guaranteed way to leak and abuse your credentials.

How do I make the widget feel fast? Stream the response token-by-token instead of waiting for the full completion, and show a typing indicator while the first token is in flight. Non-streaming requests on longer answers can feel sluggish even if total latency is similar.

Can the widget remember previous messages in a session? Yes — store the conversation array (user and assistant turns) either in the browser's session storage or server-side keyed by a session ID, and send the full history with each new request so Claude has context.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →