OpenWebSearch API
copy markdown
Use one API to query multiple web search providers. Let auto pick the provider
for each query, or name a provider or an ordered fallback list yourself. Results
come back in one normalized response either way, while the original provider
payload remains available under raw.
Base URL: https://api.openwebsearch.ai
Quickstart
Create an API key, save it as OPENWEBSEARCH_API_KEY, and send your first
search request.
export OPENWEBSEARCH_API_KEY="ows_..."Run your first search
provider: "auto" is the simplest way to start: the gateway reads the query and
picks the provider for you. The response reports which one served it.
fetch
const response = await fetch("https://api.openwebsearch.ai/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENWEBSEARCH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
provider: "auto",
query: "What changed in browser automation this week?",
max_results: 10,
}),
});
const { results, provider, usage } = await response.json();Use with a coding agent
This reference is also published as an agent skill, so an agent can integrate OpenWebSearch without being walked through the API.
npx skills add https://openwebsearch.aiTo give an agent search as a tool it can call directly, rather than an API to write code against, connect the MCP server instead.
Authentication
Send your API key as a bearer token with every API request.
Authorization: Bearer YOUR_API_KEYAuthentication failures return a standard 401 error envelope.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /v1/search | Search through one provider or an ordered fallback list. |
GET | /v1/providers | Discover providers, result limits, and parameter support. |
POST | /mcp | MCP server for agents and MCP-capable editors. |
Search request
Send a JSON body to POST /v1/search.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | Yes | A non-empty search query. |
provider | string | Yes, unless providers is set | One provider slug, or "auto" to let the query choose the provider. |
providers | string[] | Yes, unless provider is set | Ordered provider fallback list. Takes precedence over provider. |
allow_fallbacks | boolean | No | Enables fallback through providers. Defaults to true for lists with multiple entries. |
strict_params | boolean | No | Reject unsupported parameters instead of dropping them with a warning. Defaults to false. |
max_results | integer | No | Number of results. Defaults to 10 and is capped per provider. |
country | string | No | Two-letter ISO country code, such as "us". |
include_domains | string[] | No | Restrict results to these domains. |
exclude_domains | string[] | No | Exclude results from these domains. |
start_date | string | No | Earliest publication date in YYYY-MM-DD format. |
end_date | string | No | Latest publication date in YYYY-MM-DD format. |
recency | string | No | One of hour, day, week, month, or year. |
safe_search | string | No | One of off, moderate, or strict. |
provider_options | object | No | Provider-native options keyed by provider slug. |
You must provide either provider or a non-empty providers list; there is no
implicit default. Send provider: "auto" when you have no preference.
Supported providers
These are the values provider and providers accept today.
| Provider | Slug | Max results | Best at |
|---|---|---|---|
| Auto | auto | 10 | Routing each query to the best-fit provider |
| Brave | brave | 20 | Independent broad-web and media search |
| Exa | exa | 100 | People, companies, and semantic discovery |
| Perplexity | perplexity | 20 | Citation-backed, real-time answers |
| Parallel | parallel | 20 | Token-dense excerpts for AI agents |
| Valyu | valyu | 20 | Academic, financial, and proprietary data |
| Apify Serp | apify | 100 | Localized SERP features and rankings |
Slugs are case-sensitive. An unrecognized one returns a 400.
Unified response
Every successful request returns the same top-level shape. The provider field
identifies the provider that actually served the request, including when a
fallback was used or when auto chose the provider. It is always a concrete
provider slug, never "auto".
{
"id": "req-3f0c...",
"provider": "exa",
"query": "browser automation",
"results": [
{
"title": "Browser automation in 2026",
"url": "https://example.com/browser-automation",
"snippet": "A look at what changed across headless browsers this year.",
"content": "Full page content when available.",
"published_date": "2026-08-02",
"source": "example.com",
"raw": {}
}
],
"usage": {
"cost": 0.007,
"results_count": 1
},
"warnings": []
}Result fields
| Field | Type | Description |
|---|---|---|
title | string or null | Result title. |
url | string | Canonical result URL. |
snippet | string or null | Short excerpt or description. |
content | string or null | Full text when supplied by the provider. |
published_date | string or null | Provider-supplied publication date. Never fabricated. |
source | string or null | Domain or source name. |
raw | any | Original untouched provider result. |
Any provider-supplied relevance score is available only inside raw; it is not
normalized to a top-level field.
usage.cost is the USD cost of the request and usage.results_count is the
number of normalized results returned.
Automatic provider selection
Set provider to auto and the gateway inspects the query and routes it to the
provider best suited to answer it.
fetch
const response = await fetch("https://api.openwebsearch.ai/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENWEBSEARCH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "academic papers on CRISPR off-target effects",
provider: "auto",
max_results: 5,
}),
});
// "perplexity" — the provider `auto` routed to, not "auto".
const { provider } = await response.json();Billing is unchanged: the request is charged at the rate of the provider that
served it, which usage.cost reports as usual.
What works with auto
The portable filters—country, include_domains, exclude_domains,
start_date, end_date, recency, and safe_search—are all accepted and
applied to the chosen provider. Because that provider is only known once routing
has happened, any param_unsupported warning names the resolved provider rather
than auto:
{
"provider": "perplexity",
"warnings": [
{
"code": "param_unsupported",
"provider": "perplexity",
"detail": "safe_search"
}
]
}If a filter is a correctness requirement rather than a preference, pin the
provider yourself instead — with auto, which filters survive depends on where
the query is routed.
Limits and restrictions
| Constraint | Behavior with auto |
|---|---|
max_results | Capped at 10. Higher values are clamped and reported as max_results_clamped. |
providers | ["auto"] is valid, but auto cannot be combined with other slugs in the list. |
strict_params | Not supported. Sending it returns 400. |
provider_options | Not supported, since the target provider is not known ahead of time. Returns 400. |
The last two are rejected rather than ignored:
{
"error": {
"message": "Invalid request parameters: provider_options: `provider_options` is not supported with provider \"auto\"",
"type": "invalid_request_error",
"code": "invalid_request",
"request_id": "req-9b902f06..."
}
}Reach for an explicit provider or a providers fallback list when you need
any of those.
Provider selection and fallback
Set provider for a single-provider request:
fetch
const response = await fetch("https://api.openwebsearch.ai/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENWEBSEARCH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "latest advances in fusion energy",
provider: "brave",
max_results: 5,
}),
});Set providers for an ordered fallback chain:
fetch
const response = await fetch("https://api.openwebsearch.ai/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENWEBSEARCH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "latest advances in fusion energy",
providers: ["exa", "perplexity", "brave"],
allow_fallbacks: true,
max_results: 5,
}),
});The gateway returns the first successful response. It tries the next provider when a provider returns no results, times out, or has a transient failure. Invalid requests stop immediately and do not trigger fallback. Results from multiple providers are never blended.
Provider capabilities
Not every provider supports every filter in the same way. Each unified parameter has one of three support levels:
| Level | Meaning |
|---|---|
native | The provider supports the parameter directly. |
emulated | OpenWebSearch translates or approximates the parameter. |
unsupported | The provider cannot honor the parameter. |
Use capability discovery before choosing a provider or fallback order:
fetch
const response = await fetch("https://api.openwebsearch.ai/v1/providers", {
headers: {
Authorization: `Bearer ${process.env.OPENWEBSEARCH_API_KEY}`,
},
});
const { providers } = await response.json();{
"providers": [
{
"slug": "brave",
"max_results_cap": 20,
"params": {
"country": "native",
"include_domains": "emulated",
"exclude_domains": "emulated",
"start_date": "emulated",
"end_date": "emulated",
"recency": "native",
"safe_search": "native"
}
}
]
}By default, unsupported parameters are dropped and reported in warnings.
Set strict_params: true to return a 400 instead.
auto is listed alongside the providers with a max_results_cap of 10 and an
empty params object. That empty object means capability depends on the
provider chosen at request time, not that filters are ignored — read the support
levels off the provider that actually served the request.
Provider-specific options
Use provider_options when you need a native provider feature that is not part
of the unified schema. These fields are passed to the selected provider.
Gateway-owned safety and cost controls (per-provider result caps and sanitized
options) are always re-applied, so they cannot be overridden.
fetch
const response = await fetch("https://api.openwebsearch.ai/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENWEBSEARCH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "transformer architecture",
provider: "exa",
provider_options: {
exa: {
type: "fast",
contents: { text: true, highlights: true },
},
},
}),
});Provider-specific options are intentionally not portable. Keep them scoped
under the matching provider slug, and note that they cannot be combined with
provider: "auto", which does not know the target provider up front.
Warnings
Non-fatal adjustments are returned in warnings[].
| Code | Meaning |
|---|---|
param_unsupported | The provider cannot honor a requested parameter, so it was dropped. |
max_results_clamped | max_results exceeded the provider limit and was reduced. |
domains_truncated | A translated domain-filter expression exceeded the provider limit. |
recency_emulated | recency was translated into a provider date filter. |
Errors
All errors use the same envelope:
{
"error": {
"message": "Human-readable description",
"type": "invalid_request_error",
"code": "invalid_request",
"param": "query",
"provider": "exa",
"request_id": "req-3f0c..."
}
}| HTTP status | Type | Typical cause |
|---|---|---|
400 | invalid_request_error | Invalid JSON, parameters, dates, or provider. |
401 | authentication_error | Missing or invalid API key. |
403 | permission_error | The project cannot make the request. |
429 | rate_limit_error | Rate limit exceeded. |
500 | internal_error | Unexpected API error. |
502 | provider_error | A provider failed or all fallbacks were exhausted. |
503 | service_unavailable_error | No available provider for the request, or a temporary dependency outage. Retry. |
When relevant, errors include param and provider. Every response and error
carries a request id (id on success, request_id on errors) — include it when
contacting support.
Rate limits
Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and
X-RateLimit-Reset. A 429 response also includes Retry-After.
MCP server
OpenWebSearch is also a remote Model Context Protocol server, so an agent or an MCP-capable editor can call search as a tool instead of you writing an HTTP integration. It is the same gateway, the same key, and the same providers as the REST API.
Endpoint: https://api.openwebsearch.ai/mcp
The transport is Streamable HTTP and the server is stateless: every call is a
POST, no session is negotiated, and no Mcp-Session-Id is issued or expected.
GET and DELETE return 405, since there is no server-initiated stream and
no session to terminate.
Connect a client
Most clients take a URL and headers. Authenticate with the same ows_ key you
use for REST, as a bearer token:
{
"mcpServers": {
"openwebsearch": {
"url": "https://api.openwebsearch.ai/mcp",
"headers": {
"Authorization": "Bearer ows_..."
}
}
}
}If you are calling the endpoint directly rather than through a client library,
two headers are required: Content-Type: application/json, and an Accept that
lists both application/json and text/event-stream. Responses are SSE-framed,
so the JSON-RPC payload arrives on a data: line. Accept: */* is rejected with
a 406.
curl https://api.openwebsearch.ai/mcp \
-H "Authorization: Bearer $OPENWEBSEARCH_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "web_search",
"arguments": { "query": "browser automation", "max_results": 3 }
}
}'Tools
| Tool | Description |
|---|---|
web_search | Search the web and return ranked results with snippets. Billed like POST /v1/search. |
list_providers | Report each provider's result cap, parameter support, option catalog, and blocked options. Free. |
web_search takes query (the only required argument) plus the same portable
filters as the REST API: max_results, country, include_domains,
exclude_domains, start_date, end_date, recency, safe_search,
allow_fallbacks, and provider_options.
Provider selection works differently here. providers is optional — omit it and
the gateway routes through a default fallback chain, which is the recommended
path since a model has no basis for picking an index. Set it only when a
specific provider matters:
{
"query": "transformer architecture",
"providers": ["exa"],
"max_results": 5,
"include_domains": ["arxiv.org"]
}list_providers takes one optional argument, slug, to return a single
provider instead of all of them. It needs no credentials and is never billed,
so an agent can call it to discover capabilities before spending anything. It
returns more detail than GET /v1/providers does: alongside max_results_cap
and params, each entry carries notes explaining emulated or unsupported
behavior, an options JSON Schema of the full provider_options catalog, and
blocked_options mapping each refused option to the reason.
How the tool differs from POST /v1/search
The MCP tool is tuned for models rather than programmatic callers, so four things behave differently. These are the ones that will bite when porting a REST integration:
| REST | Over MCP |
|---|---|
provider (singular) | Not a parameter. It is ignored rather than rejected, and the default chain answers instead. |
provider: "auto" | Unavailable. providers accepts only concrete slugs, so ["auto"] fails validation. |
strict_params | Not a parameter. Unsupported filters are always dropped and reported in warnings. |
providers is required | Optional. Omitting it selects the gateway's default chain. |
Results are normalized exactly as they are over REST, with two adjustments that keep a tool result from swamping a context window:
rawis never included. It exists for programmatic REST consumers and is unusable by a model.contentis capped per result, with a marker noting how many characters were omitted. The key is absent entirely when the provider supplied no page text.
Every response carries both a text content block and structuredContent, which
hold the same payload: id, provider, query, results, usage, and
warnings when there are any. As with REST, provider names the index that
actually served the request.
Errors over MCP
Anything you can cause inside a tool — an invalid argument, a rejected key, a
provider outage, a rate limit — comes back as a normal tool result flagged
isError, not as a JSON-RPC fault, so the agent can read the reason and recover.
The text names the status, the error type, and whether retrying is worthwhile:
openweb web_search failed (429 rate_limit_error, retryable): Too many requests. Please try again shortly. — retry after 30s.Omitting the Authorization header entirely is the one credential case handled
at the transport layer: it returns 401 with a WWW-Authenticate challenge,
which is what prompts a client to ask for a key. A key that is present but
invalid or out of credits returns 200 with an isError result explaining
which of the two it is, because clients tend to treat a 401 as "server
unavailable" and never show the model the message.
Rate limits are the same per-project limits as REST, but MCP has no header
channel, so X-RateLimit-* headers are not sent and the retry delay rides in
the error text instead. Two transport ceilings also apply: a request body is
capped at 1 MB, and a JSON-RPC batch at 20 messages.
More examples
Filters with strict parameter handling
fetch
const response = await fetch("https://api.openwebsearch.ai/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENWEBSEARCH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "transformer architecture",
provider: "exa",
max_results: 3,
include_domains: ["arxiv.org"],
start_date: "2024-01-01",
strict_params: true,
}),
});