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.
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 nativeweb_search_callitems andresponse.web_search_call.*/response.output_text.annotation.addedstreaming events./v1/chat/completions(supported for compatibility): citations are surfaced on the final message and ondelta.annotationsin 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.
Quick start
Enable web search by addingweb_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 underweb_search_options.<field>; on Responses they go on the web_search tool entry (tools[].<field>).
Example combining several options on Chat Completions:
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 OpenAIchat.completion. Citations appear in two places:
- inline in the assistant text as ASCII markdown links (
[label](url)), - as structured
url_citationannotations on the assistant message, whosestart_index/end_indexspan the label text.
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
Theresponse.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.
{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:
response.completed event carrying the full output and usage.
Failure statuses and PII masking
A search or page fetch that fails surfaces withstatus: "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:delta.content of chat.completion.chunk frames. Each marker is a standalone line:
statusfollows a simple lifecycle: onein_progressmarker when a search or fetch starts, followed by one terminal marker (completedorfailed).action.typeissearchfor web searches andopen_pagefor page fetches. A search that triggers multiple page fetches produces one marker pair per URL.action.queryis the query the model asked for, before any masking.erroris only present onfailedmarkers.sourcesis only present on terminal markers forsearchcalls that produced results. Each entry is a{url, title}pair attributing a citation to this specific call.titlemay be an empty string; clients should fall back to the URL or hostname in that case.piiis only present on terminalsearchmarkers when the PII check ran. Its shape matches_tinfoil.pii. A marker withoutpiimeans the check did not run, not that the query was clean.
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: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:
_tinfoilis present on failed calls and onsearchcalls where the PII check ran. Successful calls with no PII check omit it entirely._tinfoil.error.codeis present on every failed call. Tool failures usetool_error._tinfoil.piiis present onsearchcalls when the PII check ran, including when nothing was masked (masked: false, emptyredactions). 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_callitems, - the streaming
response.output_item.done.itemfor aweb_search_call.
Optional: usage reporting
To have aggregated token usage surfaced as an HTTP response header or trailer, send:X-Tinfoil-Usage-Metrics:
- as a response header for non-streaming requests,
- as an HTTP trailer for streaming requests.
web_search_calls with the number of calls, and cost_usd includes the web search fee:
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
Thepii_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.
- Names alone
- Dates or addresses without a detected private person
- Business or corporate contact information
- Public figures’ public information
web_search_call item surfaces as failed.
Seeing what was masked:
- On the Responses API, read
_tinfoil.piion eachweb_search_callitem. - On Chat Completions with progress markers enabled, read
piion the terminalsearchmarker. - Direct MCP callers read
pii_checked,pii_masked,redacted_query, andpii_redactionsin the search result.
Prompt-injection protection
Theprompt_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 supportsprevious_response_id to continue a conversation across turns. Prior search results and fetched pages are carried forward so the model can build on them: