# Python examples: search, run queries in parallel, answer with citations

> Python web search API examples: requests, async httpx with a semaphore, and an answer-with-citations loop for any LLM within a token budget. No KYC.

- URL: https://llmwebsearch.com/docs/python/
- Last updated: 2026-10-22
- Publisher: LLMWebSearch (https://llmwebsearch.com)

Three scripts cover what most Python agents need: one search with `requests`, many searches at once with `httpx` and a semaphore, and a short loop that hands the sources to a language model and gets back an answer with numbered citations.

**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

| Attribute | Value |
| --- | --- |
| 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.

*search_once.py*

```python
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.

*many_searches.py*

```python
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.

*answer_with_citations.py*

```python
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](https://llmwebsearch.com/docs/errors/) 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.

---

LLMWebSearch: LLMWebSearch is a web search API for AI agents that returns ranked results with the content of the top pages in Markdown, paid in crypto with no ID upload. Prepaid credits from $1.20/1K searches never expire. No KYC — no ID, no selfie, no documents. Pay in crypto.
