← Blog

How to Build a FAQ Bot With Claude API

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

A FAQ bot built on the Claude API answers repetitive support questions by matching user queries against a known set of facts — your documentation, pricing page, policies, or support history — and generating a natural-language answer grounded in that content. The core pattern is simple: retrieve the relevant FAQ entries, pass them to Claude as context, and let the model phrase a conversational response instead of returning a flat list of matches.

This article walks through the full build: structuring your FAQ data, writing a prompt that keeps Claude grounded (so it doesn't invent answers), handling retrieval for larger FAQ sets, and shipping it as an API endpoint. The examples use plain API calls so you can adapt them to any backend, whether you're calling Claude directly or through a proxy like SubToAPI.

Step 1: Structure your FAQ content

Before writing any code, get your FAQ content into a consistent, queryable format. A simple JSON array works for most use cases:

[
  {
    "id": "refund-policy",
    "question": "What is your refund policy?",
    "answer": "We offer a 14-day money-back guarantee on all plans. Refunds are processed within 5 business days."
  },
  {
    "id": "team-seats",
    "question": "How do team seats work?",
    "answer": "Each seat gives one user their own login and API key quota. Seats can be added or removed anytime from the billing page."
  }
]

If you have more than 30-50 entries, you'll want a retrieval step (covered below) so you're not stuffing the entire FAQ into every prompt. Below that threshold, you can usually pass the whole set.

Step 2: Write a grounded system prompt

The single most important part of a FAQ bot is preventing hallucination. Claude should answer only from the provided FAQ entries and say so clearly when it can't. A system prompt like this works well:

You are a support assistant that answers questions using only the FAQ
entries provided below. Rules:

1. Answer using information from the FAQ entries only.
2. If the user's question isn't covered by the entries, say so and
   suggest they contact support — do not guess or invent an answer.
3. Keep answers to 2-4 sentences unless more detail is explicitly requested.
4. If multiple entries are relevant, synthesize them into one coherent answer.

FAQ entries:
{{faq_context}}

This structure gives Claude clear boundaries. Without rule 2, models will often try to be "helpful" by extrapolating an answer that sounds plausible but isn't accurate — which is worse than no answer at all for a support bot.

Step 3: Basic implementation without retrieval

For a small FAQ set, pass everything on every request:

async function askFaqBot(userQuestion, faqEntries) {
  const context = faqEntries
    .map(e => `Q: ${e.question}\nA: ${e.answer}`)
    .join("\n\n");

  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: 400,
      system: `You are a support assistant. Answer only from the FAQ entries below. If not covered, say so and suggest contacting support.\n\n${context}`,
      messages: [{ role: "user", content: userQuestion }]
    })
  });

  const data = await response.json();
  return data.content[0].text;
}

This is enough for most internal tools, documentation sites, or early-stage products. It's cheap to build and easy to debug because the full context is visible in every request.

Step 4: Add retrieval for larger FAQ sets

Once your FAQ grows past a few dozen entries, sending everything in every request wastes tokens and can push relevant entries out of focus. Add a retrieval step before calling Claude:

  1. Embed each FAQ entry's question (using any embedding model or a simple keyword/TF-IDF index for smaller sets).
  2. Embed the incoming user question.
  3. Retrieve the top 3-5 closest matches by cosine similarity.
  4. Pass only those matches as context to Claude.
function retrieveRelevant(userQuestion, faqEntries, topK = 4) {
  // scoredEntries computed via your embedding/similarity method
  const scored = faqEntries
    .map(e => ({ entry: e, score: similarity(userQuestion, e.question) }))
    .sort((a, b) => b.score - a.score);

  return scored.slice(0, topK).map(s => s.entry);
}

This keeps prompts short, reduces latency, and makes the bot scale to hundreds of FAQ entries without a quality drop. It also makes it easy to log which entries were retrieved for a given question, which helps you spot gaps in your FAQ coverage over time.

Step 5: Handle "I don't know" gracefully

A common failure mode is a bot that confidently answers questions it shouldn't. Test this explicitly during development by asking questions that are clearly outside your FAQ scope, and verify the model declines rather than improvises. If you see hallucinated answers, tighten the system prompt language or lower max_tokens to discourage elaboration, and consider adding a few-shot example of a correct "not covered" response directly in the system prompt.

Step 6: Stream responses for better UX

For a chat-style FAQ widget, streaming makes the bot feel responsive instead of making users wait for a full answer. Set "stream": true in the request and read the response as server-sent events rather than a single JSON blob — see the streaming docs for the event format if you're integrating against SubToAPI.

Step 7: Deploy it as an API endpoint

Wrap the logic above in a backend route (/api/faq-bot) that your frontend widget calls. Keep your model API key server-side only — never expose it in frontend JavaScript. If you're using SubToAPI, this also gives you per-key usage tracking, so you can see exactly how many requests your FAQ bot is generating and attribute cost to it separately from other features. Get started at /signup, check pricing, or follow the quickstart to get your first request working in a few minutes.

Keep iterating on the FAQ content

A FAQ bot's quality is almost entirely a function of its underlying content, not prompt cleverness. Log unanswered or low-confidence questions, review them weekly, and add new entries for recurring gaps. Over time this turns your FAQ bot into a genuinely useful first line of support rather than a glorified search box.

Questions

Does a FAQ bot need a vector database? Not necessarily. For fewer than ~50 entries, passing all FAQs as context works fine. Beyond that, simple embedding-based retrieval (even without a dedicated vector DB) improves accuracy and reduces token usage.

How do I stop Claude from making up answers? Explicitly instruct it in the system prompt to answer only from provided FAQ entries and to say "I don't know" when content isn't covered, then test with out-of-scope questions during development.

Can I use the same FAQ bot for multiple products? Yes — keep FAQ entries tagged by product or namespace, filter the retrieval step by that tag, and pass only the relevant subset into the system prompt for each request.

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 →