> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tilion.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Search

> Search-engine results as structured JSON. No session is needed: each search runs on a browser of its own and tries the engines in order until one answers.

## POST /v1/search

Search-engine results as structured JSON. No session is needed: each search runs on a browser of its own and tries the engines in order until one answers.

Auth: Bearer API key.

Request:

| Field | Type | Default | Meaning |
| - | - | - | - |
| `query` | string, 1 to 2000 characters | required | The search string. |
| `engine` | string | null | Pin one engine: `duckduckgo`, `bing`, `google` or `yahoo`. With no pin the chain `duckduckgo`, `bing`, `google` is tried in order and the first engine that answers wins. |
| `region` | string, ISO 3166-1 alpha-2 | null | Run the search from this country. Not currently available: answers 503 `region_routing_unavailable` (see `GET /v1/search/regions`). |
| `count` | integer, 1 to 100 | 10 | Results wanted. |
| `offset` | integer, 0 or more | 0 | Skip this many results, for paging. |
| `collapse_domains` | boolean | false | Keep one result per domain. |
| `fresh` | boolean | false | Skip the result cache and refresh it. |
| `async_mode` | boolean | false | Return HTTP 202 with `job_id`, `kind: "search"` and `status: "pending"`. Poll `GET /v1/job/{job_id}` for the result. |
| `session_id` | string | null | Not supported; a search always uses its own browser. Refused with 409. |

Response:

| Field | Type | Meaning |
| - | - | - |
| `query` | string | The query as sent. |
| `engine` | string | The engine that answered. |
| `count` | integer | Results in this response. |
| `offset` | integer | The offset applied. |
| `total_found` | integer | Distinct results seen before paging. |
| `has_more` | boolean | `total_found` exceeds `offset + count`. |
| `results` | array | Each `{title, url, snippet, rank, engine, domain, published}`. `rank` starts at 1 after `offset`; `domain` is the result's host without `www`; `published` is an ISO date when the engine showed one, else null. |
| `status` | string | `ok` (the engine answered; `count` may still be 0), `blocked` (the request never reached results: a challenge, a consent or deny page, or the page did not load), `error` (the engine was reached but produced nothing usable), `empty` (an operator query such as `site:` that returned nothing; not billed). |
| `reason` | string or null | The fact behind a non-`ok` status, or `no_results` on an `ok` with nothing matched: `no_results`, `blocked`, `captcha`, `rate_limited`, `access_denied`, `proxy_refused`, `page_load_failed`, `parse_empty`, `engine_error`, `unsupported_operators`, `timeout`. |
| `engines_tried` | array | Every engine tried, in order: `{engine, status, reason, took_ms}` where `status` is `ok`, `no_results`, `blocked`, `parse_empty`, `timeout`, `engine_error` or `cached`. |
| `fallback_from` | string or null | The first engine tried when the answer came from another one. |
| `cached` | boolean | The answer was served from the result cache. |
| `took_ms` | integer | Server time for the whole search. |
| `suggestion`, `operators` | string or null, array | On `reason: "unsupported_operators"`: the operators seen and a plain-language rewrite to retry. |
| `knowledge_graph`, `people_also_ask`, `related_searches`, `answer_box` | object or null, array, array, string or null | Rich blocks when the engine rendered them. |
| `region`, `exit` | string or null, object or null | Only on a regional search. |

The default chain, `{"query": "web scraping with a headless browser", "count": 5}`:

```json theme={"dark"}
{
 "query": "web scraping with a headless browser",
 "engine": "duckduckgo",
 "count": 5,
 "offset": 0,
 "total_found": 10,
 "has_more": true,
 "results": [
  {
   "title": "How to Scrape Dynamic Websites Using Headless Web Browsers",
   "url": "https://scrapfly.io/blog/posts/scraping-using-browsers",
   "snippet": "Intro to using headless web browser and libraries like Puppeteer, Playwright and Selenium in web scraping dynamic websites.",
   "rank": 1,
   "engine": "duckduckgo",
   "domain": "scrapfly.io",
   "published": null
  },
  {
   "title": "Best Headless Browsers for Web Scraping (2026 Guide)",
   "url": "https://www.browserbase.com/blog/best-web-scraping-headless-browsers",
   "snippet": "Compare the best headless browsers for web scraping : performance, detection avoidance, and scale.",
   "rank": 2,
   "engine": "duckduckgo",
   "domain": "browserbase.com",
   "published": null
  },
  {
   "title": "Headless Browser Automation Tools for Web Scraping (With Free Trials)",
   "url": "https://www.browserstack.com/guide/headless-browser-automation-tools-web-scraping",
   "snippet": "Explore the best headless browser automation tools for web scraping , including Playwright, Selenium, and managed solutions with free trials.",
   "rank": 3,
   "engine": "duckduckgo",
   "domain": "browserstack.com",
   "published": null
  },
  {
   "title": "How to Scrape Dynamic Websites with Headless Browsers in Python",
   "url": "https://www.firecrawl.dev/blog/headless-web-scraping-dynamic-websites",
   "snippet": "Learn how to scrape dynamic websites using Selenium, Playwright, and Pyppeteer in Python.",
   "rank": 4,
   "engine": "duckduckgo",
   "domain": "firecrawl.dev",
   "published": null
  },
  {
   "title": "GitHub - h4ckf0r0day/obscura: The headless browser for AI agents and ...",
   "url": "https://github.com/h4ckf0r0day/obscura",
   "snippet": "Obscura is a headless browser engine written in Rust, built for web scraping and AI agent automation.",
   "rank": 5,
   "engine": "duckduckgo",
   "domain": "github.com",
   "published": null
  }
 ],
 "status": "ok",
 "reason": null,
 "engines_tried": [
  {"engine": "duckduckgo", "status": "ok", "reason": null, "took_ms": 1233}
 ],
 "fallback_from": null,
 "cached": false,
 "took_ms": 1298,
 "suggestion": null,
 "operators": [],
 "knowledge_graph": null,
 "people_also_ask": [],
 "related_searches": [],
 "answer_box": null,
 "region": null,
 "exit": null
}
```

Pinned to Google, `{"query": "web scraping with a headless browser", "count": 3, "engine": "google"}`, with `published` dates (trimmed to the results and the attempt record):

```json theme={"dark"}
{
 "query": "web scraping with a headless browser",
 "engine": "google",
 "count": 3,
 "offset": 0,
 "total_found": 13,
 "has_more": true,
 "results": [
  {
   "title": "Your preferred method to scrape? Headless browser or ...",
   "url": "https://www.reddit.com/r/webscraping/comments/1hjuan9/your_preferred_method_to_scrape_headless_browser/",
   "snippet": "Freely Zoomable Hierarchy Search Engine · Better web scraping using headless instance of Chrome · ...",
   "rank": 1,
   "engine": "google",
   "domain": "reddit.com",
   "published": null
  },
  {
   "title": "What Is a Headless Browser and Best Ones for Web Scraping",
   "url": "https://www.zenrows.com/blog/headless-browser-scraping",
   "snippet": "Headless web scraping is a way to perform scraping with a special version of a browser with no UI, ...",
   "rank": 2,
   "engine": "google",
   "domain": "zenrows.com",
   "published": "2022-11-23"
  },
  {
   "title": "How to Scrape Dynamic Websites with Headless Browsers ...",
   "url": "https://www.firecrawl.dev/blog/headless-web-scraping-dynamic-websites",
   "snippet": "Learn how to scrape dynamic websites using Selenium, Playwright, and Pyppeteer in Python. ...",
   "rank": 3,
   "engine": "google",
   "domain": "firecrawl.dev",
   "published": "2025-12-17"
  }
 ],
 "status": "ok",
 "reason": null,
 "engines_tried": [
  {"engine": "google", "status": "ok", "reason": null, "took_ms": 8729}
 ],
 "fallback_from": null,
 "cached": false,
 "took_ms": 8857
}
```

A pinned engine has no fallback. Each engine attempt has a 20-second budget; an exhausted attempt is reported as blocked with `reason: "timeout"`. The unpinned chain can try the next engine. Use `async_mode: true` when the full search may exceed the synchronous response deadline, then poll the returned job id.

An unknown engine:

```
HTTP 400
{"detail": "unknown engine 'ask' (one of ('duckduckgo', 'bing', 'google', 'yahoo'))"}
```

Errors:

| Status | `error` or message | When |
| - | - | - |
| 400 | `unknown engine ...` | `engine` is not one of the four. |
| 402 | `insufficient_credits`, `quota_exceeded` | No credit, or the plan allowance is used up. |
| 409 | `unsupported_data_plane_command` | `session_id` was given. |
| 422 | `invalid_region`, `unsupported_region`, `region_with_session` | `region` is not a country code, no exit serves it, or it was combined with `session_id`. |
| 429 | `concurrency_limit` | No room for the session the search creates. Close sessions or retry. |
| 502 | `session_launch_failed` | The search's browser could not start; retry. |
| 503 | `region_routing_unavailable` | `region` was given. |
| 504 | `deadline_exceeded` | The search ran past 57 seconds. |

Notes:

* Timing: the default chain answers in 1 to 2 seconds; a Google pin can take around 9 seconds.
* Cache: results are cached for 120 seconds per engine, query, region, offset, count and `collapse_domains`, and `fresh: true` bypasses it. Do not depend on a repeat being served from cache or being identical.
* Billing for credit-funded accounts: an uncached search incurs the session charge and browser time, plus one action on `status: "ok"` (including zero results). A successful cached response costs one action without a new browser charge. Developer and Team have no per-call wallet debits.
* Operator queries (`site:`, `inurl:`, `OR`, ...) that return nothing come back as `status: "empty"` with `reason: "unsupported_operators"`, the `operators` seen and a `suggestion` rewrite, and are not charged the action.

## GET /v1/search/engines

The engines `POST /v1/search` accepts and the order an unpinned search tries them.

Auth: Bearer API key. No body.

```json theme={"dark"}
{"engines": ["duckduckgo", "bing", "google", "yahoo"], "default": "duckduckgo", "chain": ["duckduckgo", "bing", "google"], "max_count": 100, "cache_ttl_s": 120.0, "cache_shared": true, "engine_timeout_s": 20.0, "async_mode": true}
```

| Field | Meaning |
| - | - |
| `engines` | Values `engine` accepts. |
| `default` | The first engine of the chain. |
| `chain` | The order an unpinned search tries. `yahoo` is available only as a pin. |
| `max_count` | The ceiling for `count`. |
| `cache_ttl_s` | How long a result set is cached. |
| `cache_shared` | Whether the result cache is shared across servers. |
| `engine_timeout_s` | Per-engine attempt budget in seconds. |
| `async_mode` | Whether async search is supported. |

## GET /v1/search/regions

Countries a search can be run from, with the routes that serve each.

Auth: Bearer API key. No body.

```json theme={"dark"}
{"available": false, "order": [], "regions": {}}
```

When regional search is available, `available` is true, `order` lists the route kinds best first, and `regions` maps each country code to its routes. Until then `region` on `POST /v1/search` answers 503.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.