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.

POST/v1/chat/completions

Add 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.

Top-level field
{
  "model": "openai/gpt-5.5",
  "web_search": { "enable": true, "count": 5 },
  "messages": [ … ]
}
As a tool
{
  "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#

web_search object
FieldTypeDescription
enablebooleanDefaults to true when the object is present. Set false to turn the feature off without removing the field.
countintegerHow many results to put in front of the model, 1–20. Default 10.
search_domain_filterstring[]Restrict results to these domains, up to 20. Bare domains work best (reuters.com). The single biggest lever on answer quality.
search_recency_filterstringday, week, month, year or noLimit (default).
countrystringTwo-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.

200 OK
{
  "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
  }
}
web_search fields
FieldTypeDescription
statusstringok when the search ran. Otherwise quota_exhausted, rate_limited, unavailable or no_query — see below.
resultsarrayThe pages the model was given, in the order it saw them. Result [1] in the answer is results[0] here.
remaining_todayinteger | nullSearches left in your rolling 24-hour allowance. null when no search ran, so there is nothing to report.
noticestring | nullA 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.

200 OK — allowance used up
{
  "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."
  }
}
Non-ok statuses
FieldTypeDescription
quota_exhaustedansweredDaily allowance used up, or no plan and no wallet balance. Frees up as the rolling window moves.
rate_limitedansweredPer-minute or concurrency limit hit. Retry in a moment.
unavailableansweredSearch was briefly unavailable. Nothing is billed.
no_queryansweredNo 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.

Searches per day, and burst limits
FieldTypeDescription
Free20Then pay-as-you-go at $0.01 each, from your wallet.
Pro10010 requests/minute · 3 at a time.
Pro Plus20015 requests/minute · 3 at a time.
Max30020 requests/minute · 4 at a time.
Ultra1,50030 requests/minute · 6 at a time.
Power3,00060 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 count if 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.

Was this page helpful?