Claude API Chat Widget for Website: Build & Embed Guide
A Claude API chat widget is a small embeddable interface — usually a floating button that expands into a chat window — that lets visitors talk to Claude directly on your website. It's the same pattern as Intercom or Drift, except the "agent" is powered by Anthropic's models instead of a human or scripted bot.
To build one you need three pieces: a frontend widget (HTML/CSS/JS, or a React component) that captures user messages and renders replies, a backend endpoint that forwards those messages to the Claude API with your credentials, and a way to stream responses back so the widget feels responsive instead of freezing for several seconds per reply. This article walks through all three.
Why you can't call Claude directly from the browser
Anthropic's API keys are meant to stay server-side. If you put a key in frontend JavaScript, anyone who opens dev tools can copy it and start making requests on your bill. Every working Claude API chat widget therefore has a backend step in between — even if it's a single serverless function.
That backend step also gives you a place to:
- Set a system prompt (tone, scope, what the assistant should and shouldn't answer)
- Cap conversation length or token usage per visitor
- Log conversations for quality review
- Add rate limiting per IP or session
Minimal backend endpoint
A basic Node.js/Express proxy 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-sonnet-4-5",
max_tokens: 1024,
system: "You are a helpful support assistant for Acme Co.",
messages,
}),
});
const data = await response.json();
res.json(data);
});
This works, but every reply waits for the full response before the widget shows anything. For a chat widget that feels alive, you want streaming.
Streaming replies into the widget
Streaming sends the reply token by token using server-sent events. On the frontend, you read the stream and append text as it arrives instead of waiting for a full JSON blob. If you're routing requests through SubToAPI instead of managing raw Anthropic credentials, streaming works the same way but over a single stable HTTPS endpoint with your own sub_live_... key:
const res = 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-5",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: userMessage }],
}),
});
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);
// parse SSE lines, append text deltas to the chat bubble
}
Full details on event formats and parsing are in the streaming docs. The same endpoint also handles non-streaming calls — see the messages reference if you'd rather fetch the full reply at once for a simpler first version.
Frontend widget structure
Keep the widget itself framework-agnostic so it's easy to drop into any site — WordPress, a static site, a React app, whatever. A minimal version needs:
- A toggle button fixed to a corner of the viewport
- A message list container that auto-scrolls on new content
- An input box with Enter-to-send
- Local state holding the conversation array (
role/contentpairs) so context carries across turns
<div id="chat-widget">
<button id="chat-toggle">Chat with us</button>
<div id="chat-window" class="hidden">
<div id="chat-messages"></div>
<input id="chat-input" placeholder="Ask a question..." />
</div>
</div>
Each time the user sends a message, push it to the local messages array, send the whole array to your backend endpoint (not just the latest message — Claude needs the full history for context), and append the assistant's streamed reply as a new bubble.
Adding tool use for real functionality
A widget that only answers generic questions is limited. If you want it to look up order status, check documentation, or trigger a support ticket, you'll want tool calling — Claude decides when to call a function you've defined, your backend executes it, and the result goes back into the conversation. This is covered in the tools documentation, and it's the difference between a chat widget that sounds helpful and one that actually does something.
Rate limiting and abuse prevention
Public-facing widgets get hit by scrapers and occasionally bad actors trying to jailbreak your system prompt. Practical mitigations:
- Rate limit by session/IP (e.g., 20 messages per 10 minutes)
- Set a hard
max_tokenscap per reply - Keep the system prompt narrow and explicitly tell the model to decline off-topic requests
- Monitor token usage per conversation so one visitor can't run up unbounded costs
Where SubToAPI fits
If you already have Claude access through a subscription and want to turn it into an API your widget's backend can call — with per-key usage tracking, streaming support, and team seats if more than one person manages the integration — SubToAPI gives you a sub_live_... key and a single HTTPS endpoint instead of juggling raw Anthropic credentials and billing separately. Setup takes about the time it takes to read the quickstart guide, and there's a free trial at signup if you want to wire up a prototype widget before committing to a plan.
questions
Do I need a backend to build a Claude API chat widget, or can it run purely in JavaScript? You need a backend or serverless function. Calling the Claude API directly from browser JavaScript exposes your API key to anyone who inspects network requests, which is a security risk and usually a violation of API terms.
How do I make the widget feel fast instead of waiting for a full response? Use streaming responses (server-sent events) so text appears token by token as Claude generates it, rather than waiting for the complete reply. See the streaming docs for implementation details.
Can the widget do more than answer questions, like checking order status? Yes, with tool calling. You define functions (lookup order, create ticket, search docs) and Claude decides when to invoke them during the conversation, with your backend executing the actual logic and returning results.