# OpenWebSearch — Full Content > One API for every web search provider. Choose the provider that fits each use case and receive every result in one standardized format. This file bundles the full markdown of the OpenWebSearch documentation so a language model can ingest it in one fetch. For the link index and the integration guide, see [llms.txt](https://openwebsearch.ai/llms.txt). - **Primary site:** https://openwebsearch.ai - **API base URL:** https://api.openwebsearch.ai - **Generated:** 2026-09-23T21:24:37.914Z --- # OpenWebSearch API URL: https://openwebsearch.ai/docs 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. ```bash 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 · typescript** ```typescript 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(); ``` **requests · python** ```python import os import requests response = requests.post( "https://api.openwebsearch.ai/v1/search", headers={"Authorization": f"Bearer {os.environ['OPENWEBSEARCH_API_KEY']}"}, json={ "provider": "auto", "query": "What changed in browser automation this week?", "max_results": 10, }, ) ``` **cURL · bash** ```bash curl https://api.openwebsearch.ai/v1/search \ -H "Authorization: Bearer $OPENWEBSEARCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"provider": "auto", "query": "browser automation", "max_results": 10}' ``` ## 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. ```bash npx skills add https://openwebsearch.ai ``` To give an agent search as a tool it can call directly, rather than an API to write code against, connect the [MCP server](#mcp-server) instead. ## Authentication Send your API key as a bearer token with every API request. ```http Authorization: Bearer YOUR_API_KEY ``` Authentication 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](#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 | | You.com | `you` | 100 | Web and news search with page extraction | | Tavily | `tavily` | 20 | Fresh news and agent-ready research | | 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 | | Octen | `octen` | 100 | Real-time and multimodal web search | 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"`. ```json { "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 · typescript** ```typescript 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(); ``` **requests · python** ```python import os import requests response = requests.post( "https://api.openwebsearch.ai/v1/search", headers={"Authorization": f"Bearer {os.environ['OPENWEBSEARCH_API_KEY']}"}, json={ "query": "academic papers on CRISPR off-target effects", "provider": "auto", "max_results": 5, }, ) # "perplexity" — the provider `auto` routed to, not "auto". provider = response.json()["provider"] ``` **cURL · bash** ```bash curl https://api.openwebsearch.ai/v1/search \ -H "Authorization: Bearer $OPENWEBSEARCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "academic papers on CRISPR off-target effects", "provider": "auto", "max_results": 5 }' ``` 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`: ```json { "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: ```json { "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 · typescript** ```typescript 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, }), }); ``` **requests · python** ```python import os import requests response = requests.post( "https://api.openwebsearch.ai/v1/search", headers={"Authorization": f"Bearer {os.environ['OPENWEBSEARCH_API_KEY']}"}, json={ "query": "latest advances in fusion energy", "provider": "brave", "max_results": 5, }, ) ``` **cURL · bash** ```bash curl https://api.openwebsearch.ai/v1/search \ -H "Authorization: Bearer $OPENWEBSEARCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "latest advances in fusion energy", "provider": "brave", "max_results": 5 }' ``` Set `providers` for an ordered fallback chain: **fetch · typescript** ```typescript 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, }), }); ``` **requests · python** ```python import os import requests response = requests.post( "https://api.openwebsearch.ai/v1/search", headers={"Authorization": f"Bearer {os.environ['OPENWEBSEARCH_API_KEY']}"}, json={ "query": "latest advances in fusion energy", "providers": ["exa", "perplexity", "brave"], "allow_fallbacks": True, "max_results": 5, }, ) ``` **cURL · bash** ```bash curl https://api.openwebsearch.ai/v1/search \ -H "Authorization: Bearer $OPENWEBSEARCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "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 · typescript** ```typescript const response = await fetch("https://api.openwebsearch.ai/v1/providers", { headers: { Authorization: `Bearer ${process.env.OPENWEBSEARCH_API_KEY}`, }, }); const { providers } = await response.json(); ``` **requests · python** ```python import os import requests response = requests.get( "https://api.openwebsearch.ai/v1/providers", headers={"Authorization": f"Bearer {os.environ['OPENWEBSEARCH_API_KEY']}"}, ) providers = response.json()["providers"] ``` **cURL · bash** ```bash curl https://api.openwebsearch.ai/v1/providers \ -H "Authorization: Bearer $OPENWEBSEARCH_API_KEY" ``` ```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 · typescript** ```typescript 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 }, }, }, }), }); ``` **requests · python** ```python import os import requests response = requests.post( "https://api.openwebsearch.ai/v1/search", headers={"Authorization": f"Bearer {os.environ['OPENWEBSEARCH_API_KEY']}"}, json={ "query": "transformer architecture", "provider": "exa", "provider_options": { "exa": { "type": "fast", "contents": {"text": True, "highlights": True}, } }, }, ) ``` **cURL · bash** ```bash curl https://api.openwebsearch.ai/v1/search \ -H "Authorization: Bearer $OPENWEBSEARCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "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: ```json { "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](https://modelcontextprotocol.io) 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: ```json { "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`. ```bash 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: ```json { "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: - `raw` is never included. It exists for programmatic REST consumers and is unusable by a model. - `content` is 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: ```text 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 · typescript** ```typescript 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, }), }); ``` **requests · python** ```python import os import requests response = requests.post( "https://api.openwebsearch.ai/v1/search", headers={"Authorization": f"Bearer {os.environ['OPENWEBSEARCH_API_KEY']}"}, json={ "query": "transformer architecture", "provider": "exa", "max_results": 3, "include_domains": ["arxiv.org"], "start_date": "2024-01-01", "strict_params": True, }, ) ``` **cURL · bash** ```bash curl https://api.openwebsearch.ai/v1/search \ -H "Authorization: Bearer $OPENWEBSEARCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "transformer architecture", "provider": "exa", "max_results": 3, "include_domains": ["arxiv.org"], "start_date": "2024-01-01", "strict_params": true }' ```