Building a Claude API Agent with Web Search Tool
Claude doesn't browse the internet on its own. If you want a Claude API agent with a web search tool, you build it yourself using Claude's tool use (function calling) feature: you describe a web_search tool in your request, Claude decides when to call it, your code executes the actual search against a search provider, and you feed the results back to Claude so it can write a grounded answer.
This pattern — model calls tool, your backend runs the tool, model reads the result and continues — is the same one used for any agentic Claude API workflow, whether the tool is a database query, a calculator, or a search engine. Web search is just the most common first tool developers wire up, because it turns a static model into something that can answer questions about current events, prices, or anything released after its training cutoff.
How the agent loop actually works
Every Claude agent with tools follows the same loop:
- Send the user's message plus a
toolsarray describing what the model can call. - If Claude's response has
stop_reason: "tool_use", extract the tool call (name + input). - Run that tool in your own code — in this case, hit a search API.
- Send the tool's output back to Claude as a
tool_resultmessage. - Repeat until Claude returns a normal text response (
stop_reason: "end_turn").
Claude might call the tool once, call it multiple times for different queries, or skip it entirely if it already knows the answer. Your code just needs to handle the loop correctly — it doesn't decide when search happens, Claude does.
Defining the web_search tool
Claude needs a JSON schema describing the tool, its purpose, and its expected input. Keep the description specific — vague descriptions lead to the model calling the tool too often or not often enough.
{
"name": "web_search",
"description": "Search the public web for current information. Use this for questions about recent events, prices, release dates, or anything that may have changed after your training data.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to run"
}
},
"required": ["query"]
}
}
This schema only tells Claude that the tool exists. The actual search is something you implement separately — Claude has no built-in internet access, so you need a search provider (Brave Search API, Serper, Bing, or similar) behind this tool.
Implementing the search function
A minimal implementation just needs to call your chosen search provider and return clean, short results — titles, URLs, and snippets. Don't dump raw HTML or huge payloads into the tool result; Claude works better with a compact summary.
async function webSearch(query) {
const res = await fetch(
`https://search-provider.example.com/search?q=${encodeURIComponent(query)}`,
{ headers: { "X-API-Key": process.env.SEARCH_API_KEY } }
);
const data = await res.json();
return data.results.slice(0, 5).map(r => ({
title: r.title,
url: r.url,
snippet: r.snippet
}));
}
Wiring the loop together
Here's the full agent loop, using SubToAPI's Messages endpoint. Since SubToAPI exposes the same Messages and tool-use interface as the underlying Claude API, the tool-calling logic below is identical whether you're hitting Anthropic directly or going through SubToAPI's sub_live_... key.
const tools = [{
name: "web_search",
description: "Search the web for current information.",
input_schema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"]
}
}];
async function runAgent(userMessage) {
let messages = [{ role: "user", content: userMessage }];
while (true) {
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",
max_tokens: 1024,
tools,
messages
})
});
const data = await res.json();
messages.push({ role: "assistant", content: data.content });
if (data.stop_reason !== "tool_use") {
return data.content; // final answer
}
const toolCall = data.content.find(c => c.type === "tool_use");
const results = await webSearch(toolCall.input.query);
messages.push({
role: "user",
content: [{
type: "tool_result",
tool_use_id: toolCall.id,
content: JSON.stringify(results)
}]
});
}
}
This loop keeps going — potentially across several search calls — until Claude is satisfied it has enough information to answer. For a deeper reference on the tool schema and multi-tool setups, see the tool use docs.
Streaming the agent's final answer
Once the tool-use rounds are done and Claude is generating its final response, you'll usually want to stream that last turn to the user instead of waiting for the full completion. SubToAPI supports streaming on the same endpoint — just set stream: true on that final request and read the server-sent events as they arrive. See streaming for the event format.
Production considerations
A few things that matter once this leaves your local dev environment:
- Cap the loop. Put a hard limit (3–5 iterations) on how many tool calls the agent can make per request, so a confused model can't spin forever.
- Timeout the search provider. If your search API hangs, your whole agent request hangs. Set an aggressive timeout and return an empty result on failure rather than blocking.
- Log tool calls. When debugging why an agent gave a wrong answer, you need to see exactly what query it searched and what came back.
- Watch token usage. Search results get injected into the conversation and count toward input tokens on every subsequent call. Trim snippets aggressively.
If you're running this agent in production across a team, it's worth putting it behind a managed layer rather than juggling raw API keys per developer. SubToAPI gives each service or teammate its own sub_live_... key, tracks usage per key, and keeps the same Messages/streaming/tools interface you're already using — so the agent code above doesn't change, you just get visibility into who's calling it and how much it costs. You can try it from the signup page or check pricing for team plans.
FAQ
Does Claude have native web search built in? No. Claude's API doesn't browse the internet by itself. You implement a web_search tool with a defined schema, run the actual search in your own backend using a search provider, and return the results to Claude as a tool result.
Can the agent call web search more than once per request? Yes. Claude can issue multiple tool calls in sequence if one search isn't enough — for example, refining a query after seeing the first results. Your loop needs to handle repeated tool_use responses, usually with a max-iteration cap.
What search providers work with this pattern? Any provider that returns structured results over HTTP works — Brave Search API, Serper, Bing Search, or your own indexed data. Claude doesn't care which one you use; it only sees whatever JSON you return in the tool_result.