← Blog

Claude API Chatbot Widget for Websites: Setup Guide

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

If you want a chat bubble in the corner of your website that answers questions using Claude, you need two things: a backend endpoint that talks to the Claude API, and a small frontend widget that renders messages and streams responses. You cannot call Claude directly from browser JavaScript — API keys would be exposed to every visitor — so the widget always talks to a server you control, which then forwards requests to Claude.

This article walks through the architecture, the minimal backend you need, a working frontend widget, and the security details that matter when you're putting a chatbot in front of real website traffic.

Why you can't call Claude directly from the browser

Anthropic's API expects a secret key in the x-api-key header on every request. If you ship that key in frontend JavaScript, anyone who opens devtools can copy it and run up your bill. There's no official browser-safe way to scope an Anthropic key to "only answer questions on my pricing page."

So every Claude chatbot widget follows the same pattern:

Browser widget → Your backend endpoint → Claude API (or Claude-based proxy) → response streamed back

The backend is where you keep the key, enforce rate limits per visitor, inject your system prompt, and decide what tools (if any) the model can use.

Minimal backend endpoint

A bare-bones Node/Express endpoint that proxies to Claude looks like this:

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

  const response = await fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
      'x-api-key': process.env.ANTHROPIC_API_KEY,
      'anthropic-version': '2023-06-01',
      'content-type': 'application/json'
    },
    body: JSON.stringify({
      model: 'claude-3-5-sonnet-20241022',
      max_tokens: 500,
      system: 'You are a helpful support assistant for Acme Inc. Answer briefly.',
      messages
    })
  });

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

This works, but you still need to handle rate limiting, usage tracking per customer, and ideally per-feature API keys if multiple teams or products share the same Anthropic account. That's exactly the gap SubToAPI fills: it turns your existing Claude access into a proper HTTPS API with scoped application keys (sub_live_...), so your chatbot backend can hold a key that's limited to a specific product or environment rather than your root Anthropic credentials. See the quickstart for the exact request shape.

Streaming the response

A chat widget feels broken if the user stares at a blank bubble for three seconds. Streaming tokens as they arrive fixes that. Both the raw Anthropic API and SubToAPI support server-sent events for this:

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

  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-3-5-sonnet-20241022',
      max_tokens: 500,
      stream: true,
      messages: req.body.messages
    })
  });

  upstream.body.pipe(res);
});

Details on event formats are in the streaming docs. The widget's job on the frontend is just to read the stream and append text to the current message bubble as chunks arrive — see below.

The frontend widget

You don't need a framework for a basic widget. A floating button, a panel, an input, and a fetch call cover most use cases:

<div id="chat-widget">
  <button id="chat-toggle">Chat</button>
  <div id="chat-panel" hidden>
    <div id="chat-messages"></div>
    <input id="chat-input" placeholder="Ask a question..." />
  </div>
</div>

<script>
const messages = [];
const panel = document.getElementById('chat-panel');
const log = document.getElementById('chat-messages');
const input = document.getElementById('chat-input');

document.getElementById('chat-toggle').onclick = () => panel.hidden = !panel.hidden;

input.addEventListener('keydown', async (e) => {
  if (e.key !== 'Enter' || !input.value.trim()) return;

  const text = input.value;
  input.value = '';
  messages.push({ role: 'user', content: text });
  appendBubble('user', text);

  const bubble = appendBubble('assistant', '');
  const res = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ messages })
  });

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let full = '';
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    full += decoder.decode(value);
    bubble.textContent = full;
  }
  messages.push({ role: 'assistant', content: full });
});

function appendBubble(role, text) {
  const div = document.createElement('div');
  div.className = `bubble ${role}`;
  div.textContent = text;
  log.appendChild(div);
  return div;
}
</script>

This is intentionally plain — no build step, drop it into any site via a <script> tag or bundle it into a React/Vue component if that's your stack. The parsing above assumes the backend forwards raw text chunks; if you're consuming full SSE events you'll want to parse event:/data: lines per the streaming docs.

Things that matter in production

System prompt and scope. Keep the system prompt narrow — "only answer questions about our product, refuse everything else" — so the widget doesn't become a general-purpose assistant for competitors' prospects testing your bot.

Rate limiting per visitor. Without it, one visitor refreshing the page repeatedly can burn through your token budget. A simple IP-based or session-based limiter on your /api/chat route is enough for most sites.

Tool use for real actions. If the widget needs to look up order status or check documentation, use Claude's tool calling rather than stuffing everything into the system prompt. The tools docs cover the request format for defining functions Claude can call.

Usage visibility. Once the widget is live, you want to know how many messages it's handling and what that costs, ideally without digging through raw API logs. If you're using SubToAPI, usage metadata is attached to every response and visible in the dashboard, which makes it easier to catch runaway sessions or a widget that got embedded on a page it shouldn't be on. Check pricing for how seats and request volume map to plans, or start with the signup free trial to test a widget before committing.

Multiple API keys per environment. Staging and production widgets should not share credentials — generate separate application keys so a bug in a test deployment can't exhaust your production quota.

Questions

Can I build a Claude chat widget without a backend server? No. The Anthropic API key must stay server-side; browsers can't hold secrets safely, so you always need at least a thin proxy endpoint between your widget and Claude.

How do I stream Claude's response into the widget in real time? Set stream: true on the Messages request, pipe the server-sent events through your backend, and read the response body as a stream on the frontend, appending text as chunks arrive.

Do I need a separate API key for the chatbot vs. other Claude projects? It's strongly recommended. Scoped keys limit blast radius if one is leaked or misused, and tools like SubToAPI let you issue separate sub_live_ keys per product so usage and limits stay isolated.

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 →