# Web Search

Search the web and retrieve a ranked list of results — title, URL, snippet, and display metadata — in a single API call.

```
GET https://api.staan.ai/v2/search/web
POST https://api.staan.ai/v2/search/web
```

:::tip{title="Not sure which endpoint to use?"}

Use **Web Search** when you need a standard SERP. For RAG pipelines or LLM context, use [Web Search for AI](/docs/web-for-ai) which adds scored content chunks and full page extraction.

:::

:::info{title="GET and POST"}

Both methods are supported and behave identically: `GET` with query-string parameters, `POST` with a JSON body. Only the array-based domain filters `include_domains` / `exclude_domains` require `POST` — see [Domain filtering](#domain-filtering).

:::

## Quick start

```bash title="cURL — GET"
curl "https://api.staan.ai/v2/search/web?q=open+source+llms&market=en-us" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```bash title="cURL — POST"
curl -X POST "https://api.staan.ai/v2/search/web" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q": "open source llms", "market": "en-us"}'
```

```python title="Python"
import requests

response = requests.get(
    "https://api.staan.ai/v2/search/web",
    params={"q": "open source llms", "market": "en-us"},
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
results = response.json()["web"]["results"]
```

```typescript title="TypeScript"
const res = await fetch('https://api.staan.ai/v2/search/web', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ q: 'open source llms', market: 'en-us' }),
});
const { web } = await res.json();
```

<OpenPlaygroundButton
  server="https://api.staan.ai/v2"
  url="/search/web"
  method="GET"
  headers={[
    {
      name: 'Authorization',
      defaultValue: 'Bearer YOUR_API_KEY',
      defaultActive: true,
    },
  ]}
  queryParams={[
    { name: 'q', defaultValue: 'open source llms', defaultActive: true },
    { name: 'market', defaultValue: 'en-us', defaultActive: true },
  ]}
>
  Try Web Search
</OpenPlaygroundButton>

## Authentication

Every request must include an `Authorization` header:

```
Authorization: Bearer YOUR_API_KEY
```

## Parameters

Parameters are sent as a query string (`GET`) or a JSON body (`POST`). Both methods behave identically, except `include_domains` and `exclude_domains` which are **POST-only**.

| Parameter         | Type       | Methods    | Default | Description                                                                                                                       |
| ----------------- | ---------- | ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `q`               | `string`   | GET · POST | —       | Search query. Max 400 characters. Supports `site:` / `-site:` operators — see [Domain filtering](#domain-filtering).              |
| `market`          | `string`   | GET · POST | `fr-fr` | Language and region. See [supported markets](#supported-markets).                                                                 |
| `offset`          | `number`   | GET · POST | `0`     | Pagination offset. Values: `0`, `10`, `20`, `30` (10 results per page).                                                           |
| `count`           | `number`   | GET · POST | `10`    | Number of results. Fixed at `10`.                                                                                                 |
| `include_domains` | `string[]` | POST       | —       | Restrict results to these domains (max 10). Mutually exclusive with `exclude_domains`. See [Domain filtering](#domain-filtering). |
| `exclude_domains` | `string[]` | POST       | —       | Exclude these domains from results (max 10). See [Domain filtering](#domain-filtering).                                           |

```bash title="GET example"
curl "https://api.staan.ai/v2/search/web?q=climate+tech+startups&market=en-us" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json title="POST body example"
{
  "q": "climate tech startups",
  "market": "en-us",
  "include_domains": ["techcrunch.com", "theverge.com"]
}
```

## Domain filtering

Restrict a search to specific domains, or exclude them. Two equivalent approaches are available:

1. **Operators inside `q`** — Google-style `site:` and `-site:`. Works with **GET and POST**:

   ```bash
   curl "https://api.staan.ai/v2/search/web?q=climate+tech+site:lemonde.fr+-site:reddit.com&market=fr-fr" \
     -H "Authorization: Bearer YOUR_API_KEY"
   ```

   - `site:example.com` — keep only results from `example.com`.
   - `-site:example.com` — exclude results from `example.com`.
   - Multiple inclusions are supported, both space-separated and with `OR` chains: `site:a.com OR site:b.com`.
   - The operators are stripped from the query before it is sent to the search engine; the `query.q` returned in the response keeps your original input.

2. **Dedicated fields** — `include_domains` / `exclude_domains`. **POST only** (arrays don't map cleanly to a query string):

   ```json
   {
     "q": "climate tech",
     "market": "fr-fr",
     "include_domains": ["lemonde.fr", "lesechos.fr"]
   }
   ```

Both approaches can be combined: operators found in `q` and the dedicated fields are merged and de-duplicated.

### Which approach should I use?

The two approaches serve different intents — **who** decides the restriction:

| The restriction comes from…                                                                                        | Use                                   |
| ------------------------------------------------------------------------------------------------------------------ | ------------------------------------- |
| **The query itself** — end users typing advanced-search operators, or LLM agents that naturally emit Google syntax | `site:` / `-site:` operators in `q`   |
| **Your application** — an allowlist/blocklist defined in your config, enforced regardless of what the query says   | `include_domains` / `exclude_domains` |

In short: `site:` operators let query authors (human or AI) express source restrictions inline — they work as-is, no parsing needed on your side. The dedicated fields give your application programmatic control over the domain scope, with each entry validated as a domain.

:::warning{title="Constraints"}

- **Max 10 domains** for inclusion and 10 for exclusion. Beyond that, extra domains are dropped (the request is not rejected).
- `include_domains` and `exclude_domains` are **mutually exclusive** — do not send both.
- Domain filtering is available on **all markets** (`fr-fr`, `en-us`, `de-de`).

:::

## Supported markets

French (`fr-fr`) · English (`en-us`) · German (`de-de`)

## Response

```json title="Example response"
{
  "search_id": "01906c9e-7e3f-7000-8000-abc123def456",
  "query": {
    "q": "open source llms",
    "market": "en-us",
    "count": 10,
    "offset": 0
  },
  "web": {
    "results": [
      {
        "title": "Top open-source LLMs in 2024",
        "url": "https://www.example.com/open-source-llms",
        "snippet": "A comprehensive guide to the best open-source large language models...",
        "display_url": "www.example.com > ai > open-source-llms",
        "hostname": "www.example.com",
        "favicon_url": "https://s.qwant.com/v1/fav/..."
      }
    ]
  }
}
```

### Response fields

| Field                       | Type     | Always present | Description                                                           |
| --------------------------- | -------- | -------------- | --------------------------------------------------------------------- |
| `search_id`                 | `string` | Yes            | UUID v7 — include in support tickets.                                 |
| `query.q`                   | `string` | Yes            | Query sent by the client.                                             |
| `query.altered_query`       | `string` | No             | Present if the search engine rewrote the query.                       |
| `query.market`              | `string` | Yes            | Market used.                                                          |
| `query.count`               | `number` | Yes            | Number of results returned.                                           |
| `query.offset`              | `number` | Yes            | Offset used.                                                          |
| `web.results[].title`       | `string` | Yes            | Page title.                                                           |
| `web.results[].url`         | `string` | Yes            | Page URL.                                                             |
| `web.results[].snippet`     | `string` | Yes            | Preview from the search provider (original SERP order, not reranked). |
| `web.results[].display_url` | `string` | Yes            | Human-readable path (e.g. `example.com > blog > post`).               |
| `web.results[].hostname`    | `string` | Yes            | Domain name.                                                          |
| `web.results[].favicon_url` | `string` | No             | Website favicon, when available.                                      |
| `web.results[].thumbnail`   | `string` | No             | Page thumbnail, when provided by the search provider.                 |

## Limits

| Limit            | Value                                 |
| ---------------- | ------------------------------------- |
| Rate limit       | 20 req/s — excess traffic is rejected |
| Max query length | 400 characters                        |
| Results per page | 10 (fixed)                            |
| Max offset       | 30                                    |

:::warning

Queries over 400 characters or above the rate limit will be rejected. Implement exponential backoff in production clients.

:::

## Query tips

This endpoint works best with concise, keyword-style queries. Long, detailed prompts (e.g. _"Suggest a fresh seasonal recipe for July, for 8 people, ready in under 30 minutes"_) may yield weaker results.

For complex, natural-language questions, use [Answer](/docs/answer-v2) instead.
