Quick answer
How do I call a web search API for AI agents from Python?
POST the query to https://llmwebsearch.com/api/v1/search with requests or httpx and a Bearer key. LLMWebSearch returns ranked results with the top 3 pages as Markdown, which you can number and pass to any LLM to answer with citations. One search costs 1 credit, from $1.20/1K searches as of October 2026. Python 3.9+ and no SDK. No KYC; pay in crypto.
What you need
| Python | 3.9 or newer |
|---|---|
| Packages | requests for the simple script, httpx for the async one |
| Key | LLMWEBSEARCH_API_KEY in your environment |
| For the answer loop | Any OpenAI-compatible chat endpoint: LLM_BASE_URL, LLM_API_KEY, LLM_MODEL |
| Billing | One credit pays for one search: up to 20 ranked results, with the pages of the first 3 read and returned as Markdown. Reading 4 to 10 pages makes it 2 credits. A call that ends in an error costs nothing |
How do I run one search with requests?
Build a JSON body with q and the options you need, post it, and loop over results. Check content_status before touching content: only results marked ok carry page text. The script below saves each page that was read as a Markdown file named after its position, with the source URL on the first line.
import os
import pathlib
import requests
resp = requests.post(
"https://llmwebsearch.com/api/v1/search",
headers={"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]},
json={"q": "how to size a postgres connection pool", "max_results": 5, "read": 3},
timeout=60,
)
resp.raise_for_status()
data = resp.json()
out = pathlib.Path("sources")
out.mkdir(exist_ok=True)
for r in data["results"]:
if r["content_status"] != "ok":
print("skip", r["position"], r["content_status"], r["url"])
continue
path = out / f"{r['position']:02d}.md"
path.write_text("Source: " + r["url"] + "\n\n" + r["content"], encoding="utf-8")
print("saved", path, r["word_count"], "words")
print("credits used:", data["credits_used"])
How do I run many searches at once?
Use httpx.AsyncClient and an asyncio.Semaphore. The semaphore caps how many calls are in flight, which keeps you under your account's per-minute limit and spreads the load. Four to eight concurrent calls suit most research agents. Every account has a per-minute call limit, shown in the dashboard; ask support and it can be raised.
The example sends one search per sub-question, a typical step of a research agent, and collects the URLs of the pages that were read. Each call is independent: one failure does not stop the others.
import asyncio
import os
import httpx
API = "https://llmwebsearch.com/api/v1/search"
HEADERS = {"Authorization": "Bearer " + os.environ["LLMWEBSEARCH_API_KEY"]}
QUESTIONS = [
"what is connection pooling in postgres",
"recommended pool size per cpu core",
"pgbouncer transaction mode limitations",
"connection pool exhaustion symptoms",
]
async def search(client, sem, q):
async with sem:
resp = await client.post(API, json={"q": q, "read": 2, "max_chars": 4000})
if resp.status_code != 200:
return q, resp.json().get("error", {}).get("code"), []
data = resp.json()
read = [r["url"] for r in data["results"] if r["content_status"] == "ok"]
return q, "ok", read
async def main():
sem = asyncio.Semaphore(4)
async with httpx.AsyncClient(headers=HEADERS, timeout=60) as client:
for q, status, urls in await asyncio.gather(*(search(client, sem, q) for q in QUESTIONS)):
print(status, q, urls)
asyncio.run(main())
How do I get an answer with citations from any LLM?
Search first, then give the model numbered sources and ask it to cite them. The API returns sources, not a written answer: your own model reads the pages, writes the reply and cites the URLs, so you choose the model and the citation style. The loop below works with any chat endpoint that speaks the OpenAI-compatible chat/completions format; set its address, key and model in three environment variables.
Keep the context under control. The script trims each source so the total stays within a budget of about 6,000 tokens, counting roughly four characters per token. A result without content is passed as its snippet, labelled as such, so the model does not pretend it read the page.
import os
import requests
SEARCH = "https://llmwebsearch.com/api/v1/search"
KEY = os.environ["LLMWEBSEARCH_API_KEY"]
BUDGET_CHARS = 6000 * 4 # about 6,000 tokens of sources
def sources_for(question):
resp = requests.post(SEARCH, headers={"Authorization": "Bearer " + KEY},
json={"q": question, "max_results": 5, "read": 3}, timeout=60)
resp.raise_for_status()
results = resp.json()["results"]
share = BUDGET_CHARS // max(len(results), 1)
blocks = []
for i, r in enumerate(results, start=1):
if r["content_status"] == "ok":
body = r["content"][:share]
else:
body = "(snippet only) " + r["snippet"]
blocks.append(f"[{i}] {r['title']} - {r['url']}\n{body}")
return "\n\n".join(blocks)
def answer(question):
prompt = ("Answer the question using only the sources below. "
"Cite them as [1], [2] and so on. Say so if the sources do not answer it.\n\n"
+ sources_for(question) + "\n\nQuestion: " + question)
resp = requests.post(
os.environ["LLM_BASE_URL"].rstrip("/") + "/chat/completions",
headers={"Authorization": "Bearer " + os.environ["LLM_API_KEY"]},
json={"model": os.environ["LLM_MODEL"], "messages": [{"role": "user", "content": prompt}]},
timeout=120,
)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
print(answer("How should I size a Postgres connection pool?"))
How much text should I send to the model?
As little as answers the question. Three pages trimmed to 2,000 tokens each usually beat ten full pages: the model focuses better and each answer costs less. Lower max_chars in the search itself when you know you will trim, so less data travels.
Trimming from the start of a page works for most articles, because the main point tends to come first. For long reference pages, split the content by headings and keep the sections whose titles match the question.
What should the code do when a page is missing?
Keep going with the other sources. A page marked blocked_by_robots, blocked_by_site_policy or target_refused will not be read on a second try, so do not loop on it. A fetch_timeout or fetch_failed page may work later through https://llmwebsearch.com/api/v1/extract.
HTTP errors are different: 429, 502 and 504 deserve a retry with a growing pause. The errors page has a ready retry helper.
Python: common questions
Is there a pip package for LLMWebSearch?
No. The API is plain HTTPS and JSON, so requests or httpx is all you need. The scripts on this page are the integration: copy them into your project and adapt them.
Can I use these scripts with a local model?
Yes. Any server that exposes an OpenAI-compatible chat/completions route works with the citation loop: set LLM_BASE_URL to its address and LLM_MODEL to the model name it expects.
How do I count credits from Python?
Add up credits_used from each response, or call GET https://llmwebsearch.com/api/v1/credits before and after a job; that call is free.