# Web and X search

Search the web, fetch a page, or search live posts on X with a plain HTTP call and no model in the loop. These are the same operations that [Server tools](/en/docs/server-tools) give to a model, and the same operations the [MCP server](/en/docs/mcp) exposes to agents. All three endpoints need an API key.

## Web search

`POST /v1/web/search` returns ranked results for a query.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | yes | The search query. It must not be empty. |
| `max_results` | integer | no | How many results to return. The default is 5. A value outside 1 to 10 is moved to the nearest limit, not rejected. A negative value or one above 255 fails parsing with a plain-text `422`. |

The response has a `results` array. Each result has a `title`, a `url` and a `content` snippet. `id` identifies the request and `cost_micro_idr` is the fee charged in micro-Rupiah (Rupiah times 1,000,000). It is `0` when a plan allowance covers the call.

```json
{
  "results": [
    {
      "title": "Announcing Rust 1.87.0",
      "url": "https://blog.rust-lang.org/2025/05/15/Rust-1.87.0.html",
      "content": "The Rust team is happy to announce a new version of Rust..."
    }
  ],
  "id": "req_2c9d0a7e-5f5e-4b6a",
  "cost_micro_idr": 0
}
```

```bash
curl https://kenari.id/v1/web/search \
  -H "Authorization: Bearer $KENARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "latest stable Rust release", "max_results": 5}'
```

## Web fetch

`POST /v1/web/fetch` fetches one page and returns its text.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | The page to fetch. It must start with `http://` or `https://` and point to a public host. Addresses on private networks are refused. |

The response has the page `title`, its `content` as plain text, and `links`, a list of the URLs found on the page. `id` and `cost_micro_idr` are as above.

```json
{
  "title": "Announcing Rust 1.87.0",
  "content": "The Rust team is happy to announce a new version of Rust...",
  "links": ["https://www.rust-lang.org/", "https://blog.rust-lang.org/"],
  "id": "req_6b1f4c88-1d27-4e0b",
  "cost_micro_idr": 0
}
```

```bash
curl https://kenari.id/v1/web/fetch \
  -H "Authorization: Bearer $KENARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://blog.rust-lang.org/2025/05/15/Rust-1.87.0.html"}'
```

## X search

`POST /v1/x/search` searches live posts on X and returns a short answer with its sources.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | yes | The search query. It must not be empty. |
| `x_search_filter` | object | no | Narrows the search. Unknown fields are refused. |
| `x_search_filter.allowed_x_handles` | array of strings | no | Search only these accounts. At most 20. |
| `x_search_filter.excluded_x_handles` | array of strings | no | Leave these accounts out. At most 20. Cannot be used with `allowed_x_handles`. |
| `x_search_filter.from_date` | string | no | Start of the date range, `YYYY-MM-DD`. |
| `x_search_filter.to_date` | string | no | End of the date range, `YYYY-MM-DD`. It must not be before `from_date`. |
| `x_search_filter.enable_image_understanding` | boolean | no | Read the images inside posts. |
| `x_search_filter.enable_video_understanding` | boolean | no | Read the videos inside posts. |

Handles are written without the `@` and use only letters, digits and underscores, up to 40 characters.

The response has an `answer` and a `citations` array of `{url, title}` objects that point to x.com. `title` can be `null`. `id` and `cost_micro_idr` are as above.

```json
{
  "answer": "People are mostly discussing the new release's faster builds...",
  "citations": [
    {"url": "https://x.com/rustlang/status/1234567890", "title": null}
  ],
  "id": "req_9a40d1b3-7c52-4f1e",
  "cost_micro_idr": 0
}
```

```bash
curl https://kenari.id/v1/x/search \
  -H "Authorization: Bearer $KENARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "reactions to the latest Rust release",
    "x_search_filter": {"from_date": "2026-01-01"}
  }'
```

## Examples from code

There is no SDK method for these endpoints, so call them with `requests` or `fetch`.

### Python

```python
import os
import requests

response = requests.post(
    "https://kenari.id/v1/web/search",
    headers={"Authorization": f"Bearer {os.environ['KENARI_API_KEY']}"},
    json={"query": "latest stable Rust release", "max_results": 3},
)
response.raise_for_status()

for hit in response.json()["results"]:
    print(hit["title"], hit["url"])
```

### JavaScript

```js
const response = await fetch("https://kenari.id/v1/web/search", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.KENARI_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ query: "latest stable Rust release", max_results: 3 }),
});
if (!response.ok) throw new Error(await response.text());

for (const hit of (await response.json()).results) {
  console.log(hit.title, hit.url);
}
```

## Billing

Each successful call is billed a flat fee, separate from tokens, and the fee is returned in `cost_micro_idr`. A call that fails is not charged. A plan can cover search and fetch, and X search is billed from your balance only. See [How billing works](/en/docs/billing) and [Subscriptions](/en/docs/subscriptions) for the rules.

A key restricted to certain models needs **Web search and page fetching** ticked under **Paid capabilities** to call search and fetch, and **X search** ticked to call X search. See [Authentication and API keys](/en/docs/authentication).

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `bad_request` | `query` or `url` is empty. `x_search_filter` is invalid, and the message names the field. The `url` is not a public http or https address. The search found nothing, or the page could not be fetched. The key is restricted and was not granted the capability. The feature is switched off. |
| 402 | `insufficient_balance` | Search or fetch: the daily allowance of your plan is used up or you have no plan, and your balance cannot cover the fee. X search: your balance cannot cover the fee. |
| 429 | `rate_limit_exceeded` | A shared API key is calling too fast. Wait and retry. |
| 503 | `upstream_error` | X search failed to return a usable answer. You are not charged. Retry. |

A body that is not valid JSON, a request without `Content-Type: application/json`, and a missing or wrong-typed `query` or `url` are rejected before the endpoint runs, with a plain-text body and no `code`: `400` for invalid JSON, `415` for the missing content type, and `422` for a missing or wrong-typed field. Only an empty string reaches `bad_request`. See [Errors](/en/docs/errors#plain-text-responses).

Running out of balance on these endpoints returns `402`, the same as every other endpoint, and retrying does not help until you top up. See [Errors](/en/docs/errors) for the other codes.

## See also

- [Server tools](/en/docs/server-tools)
- [MCP server](/en/docs/mcp)
- [How billing works](/en/docs/billing)
- [Authentication and API keys](/en/docs/authentication)
