Search API
Search
Web search results as clean JSON.
Query parameters
| Name | Type | Description |
|---|---|---|
qrequired | string (1–500 chars) | The search query, passed verbatim (see Quotes and operators). |
n | integer (1–50) | Number of results to return. Default: 10. |
engines | string (comma-separated) | Comma-separated list of engines to query: bing, keenable, metacrawler, yandex, swisscows, yahoo, duckduckgo, yep, resulthunter, wikipedia, wikidata, marginalia, google, brave, qwant. See Engines below. Default: bing,keenable,metacrawler,yandex,swisscows. |
country | string (2 letters) | Two-letter ISO 3166-1 country whose results to prefer. Sets the search region and, unless language is given, the language (us → en-US, gb → en-GB, de → de-DE). Default: us. |
language | string | Search locale: language-REGION (en-US, es-US, fr-CA), a bare language (en, no region bias) or all (no language or region filter). Defaults to the country's locale; en-US for the default country. Default: en-US. |
location | string | Optional location hint, e.g. Minneapolis, MN (up to 100 characters). Appended to q unless the query already contains it. Off unless you send it. |
page | integer (1–10) | Results page. Default: 1. |
max_age | integer (seconds, 0–86400) | The oldest cached answer you accept. A cached answer younger than this is returned without querying the search engines (0 credits). 0 always queries (the fresh answer is still cached for others). Default: 21600 (6 hours). |
Country, language and location
Without country or language, searches run as country=us, language=en-US, so a city name is read as the US city. Pass country for another market: its locale becomes the language (country=gb searches as en-GB). Pass language to choose the locale yourself; it wins over country. Countries with no known locale need an explicit language, or the request is a 400. The locale is the engines' region and language signal; there is no city-level targeting.
For local lookups, send location. It is appended to the query after a space unless the query already contains the whole location or its first comma-separated part (the city), as a whole word, ignoring case. q=plumbers&location=Minneapolis, MN searches plumbers Minneapolis, MN; q=plumbers in minneapolis with the same location is searched unchanged. The response query is what was searched.
Quotes and operators
q is sent to the search engines as you wrote it, apart from trimming surrounding spaces. Quoted phrases ("Acme Roofing"), site: and -term exclusions are not stripped, escaped or rewritten. Each engine decides how strictly to honor them, and results from all queried engines are merged. When an exact phrase is rare, some engines return pages without it, so check the title or snippet for the phrase before trusting a business-name match.
A query that starts with !, ?, : or < directly followed by a word can be read by the search backend as its own syntax (engine shortcuts, a language such as :de, a timeout). Use engines and language instead, and don't start a query with those characters.
Example
curl "https://api.104-236-84-15.sslip.io/v1/search?q=headless+browser+api&n=3" \
-H "Authorization: Bearer $PAGETEER_API_KEY"const res = await fetch(`https://api.104-236-84-15.sslip.io/v1/search?${new URLSearchParams({"q":"headless browser api","n":"3"})}`, {
headers: { Authorization: `Bearer ${process.env.PAGETEER_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data, res.headers.get('x-credits-remaining'));import os
import requests
headers = {"Authorization": f"Bearer {os.environ['PAGETEER_API_KEY']}"}
res = requests.get(
"https://api.104-236-84-15.sslip.io/v1/search",
headers=headers,
params={
"q": "headless browser api",
"n": 3,
},
timeout=120,
)
res.raise_for_status()
print(res.json(), res.headers["x-credits-remaining"])Response
x-cache: MISS
x-credits-used: 2
{
"query": "headless browser api",
"quality": "complete",
"result_count": 2,
"engine_failures": [],
"results": [
{
"title": "BrowserCat | Headless Browser API",
"url": "https://www.browsercat.com/",
"snippet": "You connect to a single endpoint. BrowserCat picks the backend best suited to each job …",
"engine": "metacrawler",
"engines": ["swisscows", "metacrawler", "yandex"],
"published_date": null,
"position": 1
},
{
"title": "Headless Browser API Explained: How It Renders JavaScript Pages | Parallel",
"url": "https://parallel.ai/learn/headless-browser-api",
"snippet": "Say an AI agent needs prices from a store page. …",
"engine": "yandex",
"engines": ["swisscows", "yandex"],
"published_date": null,
"position": 2
}
],
"unresponsive_engines": [],
"cached": false,
"credits": 2
}The same request a minute later is answered from the cache: x-cache: HIT, x-cache-age: 66, "cached": true, "cache_age_s": 66 and "credits": 0.
| Name | Type | Description |
|---|---|---|
quality | complete | partial | unavailable | complete: no reported engine failures; partial: useful results with incomplete coverage; unavailable: no results with failed or skipped engines. Check this even on HTTP 200 and cache hits. |
result_count | integer | Number of results returned in this response. |
engine_failures[] | object[] | At most 8 safe { engine, category } pairs. Unknown engine names become other. Categories: timeout, rate_limited, captcha, suspended, network, upstream_error, unknown. Raw upstream reason text is excluded. |
query | string | What was searched: q, plus location when it was appended. |
results[] | object[] | Up to n results, best first: title, url, snippet, position (1-based), engine (the engine credited for it), engines (every engine that returned it) and published_date (ISO date or null). |
unresponsive_engines[] | object[] | Engines that failed, were suspended or were skipped: { engine, reason, suspended?, retry_after_s? }. Results from the others are still returned. |
cached | boolean | true when served from the search cache (0 credits). |
cache_age_s | integer | Only when cached: seconds since the answer was fetched. |
credits | integer | Credits charged; same as x-credits-used. |
HTTP 200 means an answer was returned. Check quality before treating an empty search as no matches: only complete with result_count: 0 reports an empty search without known engine failures. Use useful partial results while allowing for missing matches. Retryunavailable searches with bounded backoff; they cost 0 credits and are never cached. Cached and shared answers retain the same quality.
reason: "throttled" in unresponsive_engines means the engine was skipped for this request because it was already at its pace limit; suspended: true means we are not querying it for now (see Engines).
Page contents and answers
Add include_content=true to get the pages behind the top results in the same call, as markdown. The top content_results results are fetched in parallel (a few at a time) within one time budget, each exactly like a render with formats: ["markdown"]: the same private-network and denylist checks, robots.txt when you ask for it, unblocking tiers and response cache. Every result gets its own content with a status, so a site that blocks us or times out never fails the search.
| Name | Type | Description |
|---|---|---|
include_content | boolean | Fetch the top results as markdown in the same call. The options below need it (or include_answer); sent alone they are a 400. Default: false. |
content_results | integer (1–10) | How many of the top results to fetch (never more than were returned). Default: 3. |
markdown_mode | main | full | As on render: "full" keeps listing grids that "main" can drop. Default: main. |
max_chars | integer (100–200000) | Cut each page's markdown to this many characters (truncated: true says so). |
content_timeout_ms | integer (1000–60000) | One time budget for all pages together. A page still loading when it ends is stopped (error timeout); a page not started yet is skipped. Both are free. Default: 15000. |
content_mode | auto | http | browser | Fetch mode for the pages, as render mode. Default: auto. |
unblock | none | stealth | residential | auto | Unblocking tier for the pages, as on render. Default: none. |
respect_robots | boolean | Check each page's robots.txt first; disallowed pages fail with robots_disallowed (free). Default: false. |
content_max_age | integer (seconds, 0–86400) | Opt-in response cache for the pages, shared with render (same URL and options). Hits cost 0; identical pages fetched at the same moment share one fetch. |
include_answer | boolean | Add a short answer from the fetched pages, with citations (+40 credits when returned). Default: false. |
curl "https://api.104-236-84-15.sslip.io/v1/search?q=headless+browser+api&n=5&include_content=true&content_results=2&max_chars=4000" \
-H "Authorization: Bearer $PAGETEER_API_KEY"const res = await fetch(`https://api.104-236-84-15.sslip.io/v1/search?${new URLSearchParams({"q":"headless browser api","n":"5","include_content":"true","content_results":"2","max_chars":"4000"})}`, {
headers: { Authorization: `Bearer ${process.env.PAGETEER_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data, res.headers.get('x-credits-remaining'));import os
import requests
headers = {"Authorization": f"Bearer {os.environ['PAGETEER_API_KEY']}"}
res = requests.get(
"https://api.104-236-84-15.sslip.io/v1/search",
headers=headers,
params={
"q": "headless browser api",
"n": 5,
"include_content": "true",
"content_results": 2,
"max_chars": 4000,
},
timeout=120,
)
res.raise_for_status()
print(res.json(), res.headers["x-credits-remaining"]){
"query": "headless browser api",
"quality": "complete",
"results": [
{
"title": "BrowserCat | Headless Browser API",
"url": "https://www.browsercat.com/",
"position": 1,
…,
"content": {
"status": "ok",
"final_url": "https://www.browsercat.com/",
"http_status": 200,
"title": "BrowserCat | Headless Browser API",
"markdown": "# Headless browser API …",
"truncated": true,
"tier_used": "none",
"mode_used": "http",
"cached": false,
"credits": 1,
"ms": 812
}
},
{
"title": "Headless Browser API Explained …",
"url": "https://parallel.ai/learn/headless-browser-api",
"position": 2,
…,
"content": {
"status": "failed",
"cached": false,
"credits": 0,
"ms": 1460,
"error": { "code": "target_blocked", "message": "The target blocked every tier we tried (none). You were not charged." }
}
},
…
],
"content": { "requested": 2, "fetched": 1, "failed": 1, "skipped": 0, "credits": 1, "budget_ms": 15000, "ms": 1461 },
"search_credits": 2,
"content_credits": 1,
"cached": false,
"credits": 3
}| Name | Type | Description |
|---|---|---|
results[].content.status | ok | failed | skipped | ok: fetched and charged; failed: the fetch failed (free); skipped: the time budget ended before it started (free). |
results[].content.markdown | string | The page as markdown (status ok). |
results[].content.http_status | integer | The site's HTTP status. A 5xx is failed with error target_error. |
results[].content.error | object | { code, message }: timeout, target_blocked, captcha_required, login_required, robots_disallowed, target_rate_limited, forbidden_target, target_error, insufficient_credits, … |
results[].content.credits | integer | What this page cost. |
content | object | { requested, fetched, failed, skipped, credits, budget_ms, ms } for the whole request. |
include_answer=true (implies include_content) adds a short answer written only from the fetched pages, with [n] marks matching answer.citations: { "text": "…[1]…", "citations": [{ "index": 1, "url": "…", "title": "…" }] }. It costs 40 credits (the price of LLM extraction) when an answer is returned and needs a plan with LLM extraction (otherwise 403 feature_not_in_plan). When no page could be fetched or the model fails, answer is null, answer_error says why and the answer is free.
Engines
Without engines, a search uses bing, keenable, metacrawler, yandex and swisscows, which answered every query in our tests without blocking. Yahoo, Result Hunter, Wikipedia and Wikidata are available too, and Marginalia when the deployment has an API key. DuckDuckGo and Yep start blocking after about 20 queries, and Google, Brave and Qwant block almost at once, so these are only queried when you name them.
Pageteer never solves CAPTCHAs, so engines that only work by solving one (Mojeek, Startpage) are not offered (nor are other engines that solve proof-of-work challenges). When an engine answers with a CAPTCHA, we stop querying it for 6 hours. After "access denied", we stop for 3 hours. After "too many requests", we stop for 30 minutes. Repeated timeouts or network errors pause it for 1 to 15 minutes. Each engine is also paced: queries are spaced out and only a couple run at once across all customers, so a burst of searches gets spread out instead of hammering the upstream engine.
Caching
Search engines rate-limit and show CAPTCHAs when they see the same questions too often, and Pageteer never solves CAPTCHAs. So answers are cached for 6 hours and shared across all accounts: search results do not depend on who asks, and your query already goes to third-party search engines. A cached answer is returned with "cached": true, cache_age_s (seconds since it was fetched) and the x-cache: HIT header, and costs 0 credits.
Answers are cached in memory and the database so they survive gateway restarts. Entries expire within 24 hours of the original fetch; cache hits do not extend retention. Database TTL deletion is asynchronous.
- Queries match after trimming, collapsing whitespace and ignoring case, except inside
"quoted phrases"(kept exactly) and the operatorsOR,AND,NOT. The order ofenginesdoes not matter;n,pageandcountrymust match. max_agesets how old an answer you accept: lower it for fresher results, raise it (up to 24 hours) to reuse older ones.max_age=0always queries the engines.- Identical searches made at the same moment share one query: the others wait for it and are answered as cache hits.
- Errors and empty answers caused by failed engines are never cached. An answer where some engines failed is kept for 15 minutes at most, so the full answer comes back soon.
Credits
2 credits per search request, 0 when served from the cache. A search that returns no results because engines failed (they are listed in unresponsive_engines) is free.
With include_content, add what each fetched page costs as a render: 1 credit in HTTP mode, 5 in a browser, plus the tier add-on, 0 from the response cache. Failed and skipped pages are free, as is a page whose site answered with a 5xx; a 4xx page was fetched and is charged, as on render. credits is the total; search_credits, content_credits and answer_credits are its parts. Each page reserves its own credits, so when credits run out mid-way the remaining pages fail with insufficient_credits (free) and the search still answers.
Errors
| Status | Code | When | Charged | What to do |
|---|---|---|---|---|
| 400 | invalid_request | A parameter is invalid: an unknown engine, a country with no known locale and no language, n over 50, … | No | Fix the request; retrying it unchanged fails again. |
| 401 | invalid_api_key | No key, an unknown key, or a revoked or expired key. | No | Check the Authorization header and the key on the API keys page. |
| 402 | insufficient_credits | Not enough credits left this month for this request, and billable usage is off. error.reason says why: free_credits_used, payment_failed or subscription_inactive. | No | Act on the reason at error.billing_url: add a card, update the payment method, or restart the subscription. Or wait for the 1st (UTC). |
| 402 | spend_limit_reached | Billable usage is on and this month’s spend limit is reached (error.reason: spend_limit). | No | Raise the spend limit at error.billing_url (the Billing page), or wait for the 1st (UTC). |
| 403 | org_suspended | The account is suspended. | No | Contact support. |
| 429 | rate_limited | Over your requests per minute, or too many requests to one site across the platform right now. | No | Wait Retry-After seconds, then back off with jitter. |
| 429 | concurrency_limit | Too many of your requests are in flight at once. | No | Wait for one to finish (Retry-After: 1) or lower your parallelism. |
| 500 / 502 / 503 | internal_error | The search backend failed. | No | Retry with backoff (honor Retry-After); quote x-request-id if it persists. |
| 504 | timeout | The search backend did not answer in time. | No | Retry, or name fewer engines. |