How to Build a FAQ Chatbot with Claude
A FAQ chatbot built with Claude answers customer questions using your own documentation instead of generic model knowledge. You feed Claude your help center content as context, write a system prompt that constrains it to that content, and expose the result through an API endpoint your frontend calls. This article walks through each step: structuring your FAQ data, writing the prompt, handling the API call, and deploying it behind a real backend.
The core challenge isn't getting Claude to answer questions — it's getting it to answer only the questions you've prepared for, in the tone you want, without hallucinating policies that don't exist. That's a prompt and architecture problem more than a model problem, and it's solvable with a fairly small amount of code.
Step 1: Structure your FAQ content
Before writing any code, turn your existing FAQ, help docs, or support macros into a clean reference format. Claude performs best when the source material is plain and well-labeled rather than scraped HTML.
A simple structure works fine for most teams:
[
{
"question": "How do I reset my password?",
"answer": "Go to Settings > Security and click 'Reset Password'. You'll receive an email link valid for 24 hours."
},
{
"question": "Do you offer refunds?",
"answer": "We offer full refunds within 14 days of purchase. Contact support@yourapp.com with your order ID."
}
]
For under ~50 FAQ entries, you can pass the whole list directly in the system prompt on every request — no retrieval system needed. If you have hundreds of entries, you'll want a lightweight retrieval step (keyword match or embeddings) that picks the 5–10 most relevant entries before calling Claude. Start simple; add retrieval only when you actually outgrow direct inclusion.
Step 2: Write a system prompt that keeps Claude on-topic
This is the part most "wrap an LLM in a chat window" tutorials skip, and it's the part that actually determines whether your bot is useful or embarrassing.
You are a support assistant for [Product Name]. Answer questions
using ONLY the FAQ entries provided below.
Rules:
- If the answer isn't in the FAQ entries, say you don't have that
information and suggest contacting support@yourapp.com.
- Never invent pricing, policies, or features not listed below.
- Keep answers under 3 sentences unless the question requires detail.
- Match the user's tone but stay professional.
FAQ entries:
{{faq_json}}
The "answer ONLY using the provided entries" instruction is doing the heavy lifting here. Without it, Claude will happily draw on general knowledge about SaaS products and produce a plausible-sounding but wrong answer about your refund policy. With it, Claude defaults to "I don't have that information" when the FAQ doesn't cover the question — which is the correct failure mode for a support bot.
Step 3: Call the API
Once you have your system prompt and FAQ data assembled, the actual request is straightforward. Here's an example using SubToAPI, which turns your Claude access into a standard HTTPS endpoint with an API key, so you don't need separate provider credentials for a side project or client app:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 300,
"system": "You are a support assistant... [FAQ entries here]",
"messages": [
{"role": "user", "content": "Can I get a refund after 20 days?"}
]
}'
The response contains Claude's answer plus usage metadata (token counts) you can log for cost tracking. Full request/response details are in the docs and the messages reference if you want to see every parameter.
Step 4: Stream responses for a better UX
Support widgets feel sluggish if users stare at a blank bubble while the full answer generates. Streaming fixes this by sending tokens as they're produced:
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-5",
max_tokens: 300,
stream: true,
system: systemPrompt,
messages: [{ role: "user", content: userQuestion }]
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
renderChunk(decoder.decode(value));
}
See /docs/streaming for the full event format. For a chat widget, this one change — streaming instead of waiting for the full response — usually matters more to perceived quality than any prompt tweak.
Step 5: Add escalation and logging
A FAQ bot that can't hand off to a human is incomplete. Add a simple rule: if Claude's response contains a phrase like "contact support" (or you use a structured output asking Claude to flag "escalate": true in cases it can't resolve), route that conversation to your support queue instead of ending the chat there.
Also log every question and answer pair, even just to a database table. This log becomes your best source for improving the FAQ — you'll quickly see which questions users ask that aren't covered, and you can add those as new entries.
Step 6: Deploy behind your own backend, not the frontend
Never call the Claude API directly from client-side JavaScript — your API key would be exposed in the browser. Run the chatbot logic in a backend route (Next.js API route, Express endpoint, serverless function) that holds the key server-side and only exposes a /chat endpoint to your frontend. If you're using SubToAPI, each team member or environment can get its own sub_live_... key, so you can rotate keys per environment without touching code — see /docs/quickstart for the setup, and /pricing if you're deciding between Solo and Team plans based on how many people need dashboard access.
Questions
Does Claude need to be fine-tuned to act as a FAQ bot? No. A well-written system prompt with your FAQ content embedded is sufficient for most use cases. Fine-tuning adds cost and complexity without meaningfully improving accuracy for this task.
How do I stop the bot from answering questions outside my FAQ? Explicitly instruct it in the system prompt to answer only from the provided entries and to say "I don't have that information" otherwise. Test this with deliberately off-topic questions before launch.
What happens when my FAQ list gets too large for one prompt? Add a retrieval step: index your FAQ entries (keyword search or embeddings) and pass only the top-matching entries into the prompt per request, instead of the full list every time.