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 give to a model, and the same operations the MCP server exposes to agents. All three endpoints need an API key.
Web search
Section titled “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.
{ "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}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
Section titled “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.
{ "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}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
Section titled “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.
{ "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}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
Section titled “Examples from code”There is no SDK method for these endpoints, so call them with requests or fetch.
Python
Section titled “Python”import osimport 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
Section titled “JavaScript”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
Section titled “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 and 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.
Errors
Section titled “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. |
| 429 | plan_limit_reached | Search or fetch: the plan’s daily allowance is used up and PAYG overflow is off or 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.
Running out of balance on these endpoints returns 429, not 402. See Errors for the other codes.