Skip to main content

Overview

Tinfoil’s web search lets models augment their answers with fresh information from the web. Search runs inside a secure enclave and talks directly to Exa, a search provider with Zero Data Retention (ZDR). That gives you:
  • Query privacy: queries go from the enclave to Exa over TLS. Tinfoil never sees the query contents in plaintext.
  • User anonymity from the search provider: all users share a single enclave-held API key, so Exa only sees the enclave’s IP address.
  • Legal protections: Exa’s ZDR agreement ensures queries are never written to persistent storage or sent to external subprocessors.
  • Optional PII protection: a privacy filter can remove detected sensitive information from outgoing queries before they are sent to Exa.
  • Optional prompt-injection protection: a safeguard model can scan search results and fetched pages for prompt-injection attempts and filter them out before the model sees them.
You turn web search on per request. The response you get back is a normal chat completion (or Responses API response) with citations already attached - you do not need to handle any tool calls yourself.
Read our blog post on private AI web search for background.

Choosing an API surface

You can enable web search on either of Tinfoil’s OpenAI-compatible endpoints:
  • /v1/responses (recommended): search progress and citations are surfaced through OpenAI’s native web_search_call items and response.web_search_call.* / response.output_text.annotation.added streaming events.
  • /v1/chat/completions (supported for compatibility): citations are surfaced on the final message and on delta.annotations in streaming. The OpenAI Chat Completions spec has no native live-progress event for web search, so if you want a live progress UI there, see Streaming progress markers.
To manage the tool loop yourself, call the underlying MCP web-search server directly instead.

Quick start

Enable web search by adding web_search_options to a Chat Completions request, or a web_search tool to a Responses request. Optionally add pii_check_options and/or prompt_injection_check_options to enable the safety filters.

Chat Completions (/v1/chat/completions)

Responses (/v1/responses)

Request options

Top-level request fields

Search tuning fields

These fields are all optional. They have the same meaning on both APIs. On Chat Completions they go under web_search_options.<field>; on Responses they go on the web_search tool entry (tools[].<field>). Example combining several options on Chat Completions:
The equivalent on the Responses API:
Safety filters are opt-in per request. Include pii_check_options to mask detected PII in queries before they reach Exa, and prompt_injection_check_options to filter injection attempts out of search results and fetched pages. Both are independent; enabling one does not enable the other.

Response format

Chat Completions

The response is a standard OpenAI chat.completion. Citations appear in two places:
  • inline in the assistant text as ASCII markdown links ([label](url)),
  • as structured url_citation annotations on the assistant message, whose start_index / end_index span the label text.
Non-streaming example:
In streaming mode, the same citations arrive as delta.annotations entries interleaved with delta.content:
The Chat Completions stream is a standard chat.completion.chunk stream with no custom top-level events. If you want live search progress on Chat Completions, see Streaming progress markers.

Responses

The response.output array contains a web_search_call item for each search or page fetch, followed by the assistant message item. Citations are attached to each output_text content part as flat url_citation annotations.
The annotation shape is flat on Responses ({type, url, start_index, end_index, title}) whereas Chat Completions nests it under url_citation. When the request opts in with include: ["web_search_call.action.sources"], each search-kind web_search_call also carries the URLs the search returned:

Streaming events (Responses)

Responses streaming emits OpenAI’s standard events. The ones relevant to web search are: Assistant text streams through the usual response.output_text.delta and response.output_text.done events. Citations arrive as response.output_text.annotation.added:
The stream terminates with a single response.completed event carrying the full output and usage.

Failure statuses and PII masking

A search or page fetch that fails surfaces with status: "failed" on the terminal web_search_call item (OpenAI’s web_search_call.status enum is in_progress, searching, completed, failed). PII masking does not produce a failed status: search continues with the sanitized query and the item completes normally. The masking report rides on the optional _tinfoil sidecar as _tinfoil.pii. A failed PII check surfaces as failed with _tinfoil.error.code: "tool_error".

Optional: streaming progress markers (Chat Completions)

The OpenAI Chat Completions spec has no native event for web-search progress. If you want a live progress UI on Chat Completions (a spinner, a “searching the web…” line, a per-URL fetch indicator), you can opt in to Tinfoil progress markers. Send the request header:
When markers are enabled, progress payloads are carried as tagged JSON inside the normal delta.content of chat.completion.chunk frames. Each marker is a standalone line:
The payload shape is:
  • status follows a simple lifecycle: one in_progress marker when a search or fetch starts, followed by one terminal marker (completed or failed).
  • action.type is search for web searches and open_page for page fetches. A search that triggers multiple page fetches produces one marker pair per URL. action.query is the query the model asked for, before any masking.
  • error is only present on failed markers.
  • sources is only present on terminal markers for search calls that produced results. Each entry is a {url, title} pair attributing a citation to this specific call. title may be an empty string; clients should fall back to the URL or hostname in that case.
  • pii is only present on terminal search markers when the PII check ran. Its shape matches _tinfoil.pii. A marker without pii means the check did not run, not that the query was clean.
The same marker pairs are also prefixed onto the assistant content string in non-streaming Chat Completions responses when the header is set, so you can render an identical progress timeline from either mode.
Clients that do not parse the markers will render them as text inside the assistant message. Either parse and strip them, or leave the header off to get a pristine stream with no markers.

Consuming markers on the client

A single regex is enough to extract and strip markers. The leading and trailing newlines are absorbed so the text before and after a marker collapses seamlessly:
The marker channel is Tinfoil-specific. If you want a portable progress UI, prefer the Responses API’s native response.web_search_call.* events instead.

Optional: the _tinfoil sidecar

On the Responses API, web_search_call.status is restricted to OpenAI’s enum: in_progress, searching, completed, failed. Tinfoil exposes richer information through an optional vendor-extension field named _tinfoil that rides alongside the envelope:
Rules:
  • _tinfoil is present on failed calls and on search calls where the PII check ran. Successful calls with no PII check omit it entirely.
  • _tinfoil.error.code is present on every failed call. Tool failures use tool_error.
  • _tinfoil.pii is present on search calls when the PII check ran, including when nothing was masked (masked: false, empty redactions). Its absence means the check did not run, not that the query was clean.
_tinfoil.pii fields: action.query is always the query the model asked for. Masking follows the model’s detected span boundaries and may cover only part of a sensitive value, so neither masked: true nor the presence of pii guarantees the remaining query is free of PII. See PII protection for the masking policy. Strict OpenAI SDKs treat unknown object keys as no-ops, so _tinfoil is invisible unless you read it explicitly. The sidecar appears on:
  • the non-streaming response.output[*] web_search_call items,
  • the streaming response.output_item.done.item for a web_search_call.

Optional: usage reporting

To have aggregated token usage surfaced as an HTTP response header or trailer, send:
Tinfoil returns the usage summary in X-Tinfoil-Usage-Metrics:
  • as a response header for non-streaming requests,
  • as an HTTP trailer for streaming requests.
When the model performed at least one search or page fetch, the summary includes web_search_calls with the number of calls, and cost_usd includes the web search fee:
The fee is charged once per request regardless of how many calls the model made. If web_search_calls is absent, the model did not use web search and no fee applies, even if web search was enabled on the request. This lets a proxy server attribute web search costs without decrypting the response body. If you also want the final usage-bearing chat.completion.chunk on streaming Chat Completions, request it through standard OpenAI stream_options:

PII protection

The pii_check_options field enables filtering of outgoing search queries. The privacy filter detects spans of personally identifiable information, and a policy removes selected spans before sending the remaining query to Exa. It does not reject the whole search just because PII was detected. Masking policy:
  • Detected personal emails (private_email), personal phone numbers (private_phone), account numbers (account_number), and secrets (secret) are removed.
  • Detected private addresses (private_address) and dates (private_date) are removed when the same query also identifies a private person (private_person). The person’s name is not itself removed.
Left unchanged by this policy:
  • Names alone
  • Dates or addresses without a detected private person
  • Business or corporate contact information
  • Public figures’ public information
The policy depends on the model’s detected categories; it is not a guarantee that every sensitive value will be recognized. Generic descriptions without identifying details are allowed. If masking leaves an empty or whitespace-only query, Exa is not called and the search returns no results. If the PII check fails, the query is not sent to Exa and the web_search_call item surfaces as failed. Seeing what was masked:
  • On the Responses API, read _tinfoil.pii on each web_search_call item.
  • On Chat Completions with progress markers enabled, read pii on the terminal search marker.
  • Direct MCP callers read pii_checked, pii_masked, redacted_query, and pii_redactions in the search result.
All three carry the same report: whether spans were masked, the sanitized query, and the removed spans as offsets into the original query. If the report is absent, filtering status is unknown.

Prompt-injection protection

The prompt_injection_check_options field runs a safeguard model over each search snippet and fetched page before the content is handed back to the responding model. Results and pages that contain instructions aimed at hijacking the model (for example “ignore previous instructions”, embedded tool-use directives, or credential-exfiltration prompts) are dropped. When every result for a query is filtered out, the model sees an empty result set for that search and answers without web grounding. Fetch failures due to injection filtering surface as status: "failed" on the corresponding web_search_call item. Prompt-injection filtering is opt-in per request. Omit the field to skip the check.

Multi-turn conversations (Responses API)

The Responses API supports previous_response_id to continue a conversation across turns. Prior search results and fetched pages are carried forward so the model can build on them: