Web search in chat
One flag on a normal chat request. The model answers from live results instead of memory, and tells you which pages it used.
/v1/chat/completionsAdd web_search to any chat request and we run the search before the model does, then hand it the results as numbered, linked context. You get back a normal chat completion plus a web_search object listing the exact pages the answer was built from.
This works with every model in the catalog, including ones with no native tool calling — the results arrive as context, so there is nothing for the model to support.
curl https://api.xkiro.com/v1/chat/completions \
-H "Authorization: Bearer $XKIRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-5.5",
"web_search": { "enable": true, "count": 5 },
"messages": [
{ "role": "user", "content": "What shipped in the latest Node.js LTS? Cite your sources." }
]
}'Two ways to turn it on#
Both forms are equivalent. Use whichever your existing code already speaks — if you are moving an integration across, you should only have to change the base URL.
{
"model": "openai/gpt-5.5",
"web_search": { "enable": true, "count": 5 },
"messages": [ … ]
}{
"model": "openai/gpt-5.5",
"tools": [
{ "type": "web_search", "web_search": { "count": 5 } }
],
"messages": [ … ]
}Off unless you ask for it
Omit the field and nothing searches — we never spend your allowance on a request that did not ask for it. Send "enable": false to turn it off explicitly, which is handy when the flag is coming from a user-facing toggle.
Options#
| Field | Type | Description |
|---|---|---|
enable | boolean | Defaults to true when the object is present. Set false to turn the feature off without removing the field. |
count | integer | How many results to put in front of the model, 1–20. Default 10. |
search_domain_filter | string[] | Restrict results to these domains, up to 20. Bare domains work best (reuters.com). The single biggest lever on answer quality. |
search_recency_filter | string | day, week, month, year or noLimit (default). |
country | string | Two-letter ISO country code, e.g. VN, to bias results toward a region. Any other value is ignored rather than guessed at. |
Response#
The completion is unchanged — same shape your SDK already parses. We add one extra top-level field.
{
"id": "chatcmpl-130dde45-…",
"object": "chat.completion",
"choices": [ { "message": { "content": "Node.js 26 LTS shipped … [1][2]" } } ],
"usage": { "prompt_tokens": 2607, "completion_tokens": 154 },
"web_search": {
"status": "ok",
"results": [
{
"title": "Node.js 26 is now LTS",
"url": "https://nodejs.org/en/blog/release/v26.0.0",
"snippet": "Node.js 26 has been promoted to Long Term Support…",
"source": "nodejs.org",
"faviconUrl": "https://…/favicon.png"
}
],
"remaining_today": 19,
"notice": null
}
}| Field | Type | Description |
|---|---|---|
status | string | ok when the search ran. Otherwise quota_exhausted, rate_limited, unavailable or no_query — see below. |
results | array | The pages the model was given, in the order it saw them. Result [1] in the answer is results[0] here. |
remaining_today | integer | null | Searches left in your rolling 24-hour allowance. null when no search ran, so there is nothing to report. |
notice | string | null | A short, already-worded sentence to show a user when the search did not run. null on success. |
Streaming: the sources arrive first
On "stream": true the web_search object is delivered in the first chunk, before any text, so you can render the source list while the answer is still being written. That chunk is a normal chat.completion.chunk with an empty delta, so SDKs that do not know about the field skip over it without complaining.
When search is unavailable, you still get an answer#
If your allowance is used up, you hit a rate limit, or search is briefly down, we do not fail the request. You asked a question and switched a feature on; the feature being unavailable is not a reason to throw the question away. The model answers anyway — and it is told, in the prompt, to say plainly that it could not search and to invent nothing.
{
"choices": [
{
"message": {
"content": "I could not search the web this time.\n\nThe capital of France is Paris. As of my last knowledge update…"
}
}
],
"web_search": {
"status": "quota_exhausted",
"results": [],
"remaining_today": null,
"notice": "Daily web search allowance used up — answered without live results. It frees up as the rolling 24-hour window moves, or upgrade your plan for more."
}
}| Field | Type | Description |
|---|---|---|
quota_exhausted | answered | Daily allowance used up, or no plan and no wallet balance. Frees up as the rolling window moves. |
rate_limited | answered | Per-minute or concurrency limit hit. Retry in a moment. |
unavailable | answered | Search was briefly unavailable. Nothing is billed. |
no_query | answered | No user message to search for — e.g. the turn was system-only. |
Check status before you claim the answer is current
A 200does not mean a search ran. If you show a “searched the web” badge in your UI, drive it from status === "ok" and show notice otherwise. Presenting a from-memory answer as a live one is the failure mode this field exists to prevent.
Allowances and pricing#
A chat request with search on costs exactly one search, no matter how long the answer is — we run the search once, up front, rather than letting the model decide how many times to call a tool. It draws from the same daily allowance as the standalone endpoint.
| Field | Type | Description |
|---|---|---|
Free | 20 | Then pay-as-you-go at $0.01 each, from your wallet. |
Pro | 100 | 10 requests/minute · 3 at a time. |
Pro Plus | 200 | 15 requests/minute · 3 at a time. |
Max | 300 | 20 requests/minute · 4 at a time. |
Ultra | 1,500 | 30 requests/minute · 6 at a time. |
Power | 3,000 | 60 requests/minute · 8 at a time. |
- The search and the completion are billed separately: one search against your web allowance, plus the usual tokens for the model.
- Results add to your prompt tokens — roughly 2,000–3,000 for ten results. Lower
countif that matters more to you than breadth. - A search that did not run is never billed, and never counts against the allowance.
Practical notes
- We search the last user message, as written. Long preambles make weak queries — if you control the prompt, put the searchable question last.
- Ask for citations in your own prompt too. The model is already instructed to cite
[1],[2], but saying so again reliably improves how consistently it does. - Need the full text of a page rather than a snippet? Call web fetch with the URL you got back.
- Want the raw results without a model in the loop — to rank or cache them yourself? Use POST /v1/search.
