> SpecStream is the live web data engine for teams and agents. This is the Markdown copy of https://specstream.ai/docs/search-api. Every page, with what SpecStream does and costs: https://specstream.ai/llms.txt

# Search API

GET /v1/web-search answers a question from the live web and returns the answer, rows, and search results read.

## Make a request

Write `q` in everyday words, in any language, 1 to 2,000 characters. Quoted phrases are searched as phrases, and results that hold them come first. Create a key under **Agents & API** in your workspace. [Authentication](https://specstream.ai/docs/api.md#authentication)

Terminal

```bash
curl -G https://api.specstream.ai/v1/web-search \
  -H "Authorization: Bearer $SPECSTREAM_API_KEY" \
  --data-urlencode "q=Who is the CEO of Stripe?"
```

Python

```python
import os
from specstream_client import Client

client = Client(api_key=os.environ["SPECSTREAM_API_KEY"])
result = client.web_search(q="Who is the CEO of Stripe?")
print(result["answer"]["value"])
```

typescript

```typescript
import { Client } from "@specstream/sdk"

const client = new Client({ apiKey: process.env.SPECSTREAM_API_KEY! })
const result = await client.webSearch({ q: "Who is the CEO of Stripe?" })
console.log(result.answer?.value)
```

On MCP, call the `search_web` tool. On the command line, run `specstream web-search --q "<the question>"`. [SDKs & CLI](https://specstream.ai/docs/sdk.md)

## Price and retries

Each call is $5 per 1,000 searches, from your balance. It needs no quote and no approval. Send an `Idempotency-Key` header (at most 128 characters) to retry safely: a call is charged once per key, and the same key again gets `409`. Without one, SpecStream gives the call an ID and returns it as `request_id`.

## The reply

Over REST the reply sits under `data`. This is a real one, cut short:

JSON

```json
{
  "data": {
    "kind": "fact",
    "query": "Who is the CEO of Stripe",
    "answer": {
      "value": "Patrick Collison",
      "page": "https://en.wikipedia.org/wiki/Patrick_Collison",
      "seen_at": "2026-11-05T01:06:52+00:00"
    },
    "rows": [
      {
        "name": "Patrick Collison",
        "page": "https://en.wikipedia.org/wiki/Patrick_Collison",
        "probability": 0.64
      }
    ],
    "sources": [
      {
        "url": "https://en.wikipedia.org/wiki/Patrick_Collison",
        "title": "Patrick Collison - Wikipedia",
        "site": "en.wikipedia.org",
        "cited": true
      },
      {
        "url": "https://www.forbes.com/profile/patrick-collison/",
        "title": "Patrick Collison - Forbes",
        "site": "forbes.com",
        "cited": false
      }
    ],
    "took_ms": 2404,
    "price_usd": 0.005
  }
}
```

| Field | What it holds |
| --- | --- |
| `kind` | fact for a question with one answer, list for a request for many results. |
| `query, columns` | How the request was read, and the facts it asks for. |
| `answer` | For a fact: the value, the page it was read from, and its read time in seen\_at. It is null unless two sites print the value or the vote rates it likelier than not. Every value found stays in rows. |
| `rows` | Each result: name, page, snippet, the probability that it matches, the search results that name it in sources, and fields with the facts the request asks for. Each value has the page it was read from and its read time in seen\_at. |
| `sources` | The search results read, in rank order, each with url, title, site, snippet, and published when the result gives one. cited is true for each one an answer, row, or value comes from. |
| `handoff` | For a list: the full list as a find. rows\_estimated is the size. rows\_basis says where it comes from: request, page, or seen. next is the call to make. |
| `passages` | With passages=true: the text of the top pages closest to the request, each with url and title. The call takes up to about a second more. |
| `id, request_id, took_ms, price_usd` | The ID the call is charged under, the time it took, and its price. |

To get a `list` in full, call `next` from the `handoff`, then quote and approve it. [Deep Search](https://specstream.ai/deep-search.md)

## Search history

Every charged call stays in the account’s history. `GET /v1/web-search/answers` lists them newest first, and `q` keeps the searches whose words contain it (`limit` 1 to 50, then follow `next_cursor`). `GET /v1/web-search/answers/{id}` returns one as it was given. On MCP, use `list_web_searches` and `get_web_search`. An account API key with `finds:read` reads the history; a project key does not. Reading the history costs nothing.

## When a call stops

| Status | What happened |
| --- | --- |
| `401` | No key or token. |
| `402` | The balance is empty, or the call would go over the account’s monthly spending limit. |
| `403` | The call would go over the limit delegated to the account or the key. |
| `409` | The Idempotency-Key was already answered. Send a new one. |
| `422` | q is empty or over 2,000 characters, or the key is over 128. |
| `503` | The account used its calls for the minute, or search is busy. Wait the seconds in Retry-After and send it again. Nothing is charged. |
