Skip to content
kenari.

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.

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

FieldTypeRequiredDescription
querystringyesThe search query. It must not be empty.
max_resultsintegernoHow 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
}
Terminal window
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}'

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

FieldTypeRequiredDescription
urlstringyesThe 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
}
Terminal window
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"}'

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

FieldTypeRequiredDescription
querystringyesThe search query. It must not be empty.
x_search_filterobjectnoNarrows the search. Unknown fields are refused.
x_search_filter.allowed_x_handlesarray of stringsnoSearch only these accounts. At most 20.
x_search_filter.excluded_x_handlesarray of stringsnoLeave these accounts out. At most 20. Cannot be used with allowed_x_handles.
x_search_filter.from_datestringnoStart of the date range, YYYY-MM-DD.
x_search_filter.to_datestringnoEnd of the date range, YYYY-MM-DD. It must not be before from_date.
x_search_filter.enable_image_understandingbooleannoRead the images inside posts.
x_search_filter.enable_video_understandingbooleannoRead 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
}
Terminal window
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"}
}'

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

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"])
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);
}

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.

StatusCodeWhen
400bad_requestquery 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.
429plan_limit_reachedSearch 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.
429rate_limit_exceededA shared API key is calling too fast. Wait and retry.
503upstream_errorX 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.