Skip to content
Docs menu

Search API

Search

Web search results as clean JSON.

GET https://api.104-236-84-15.sslip.io/v1/search

Query parameters

Search parameters
NameTypeDescription
qrequiredstring (1–500 chars)The search query, passed verbatim (see Quotes and operators).
ninteger (1–50)Number of results to return. Default: 10.
enginesstring (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.
countrystring (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.
languagestringSearch 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.
locationstringOptional location hint, e.g. Minneapolis, MN (up to 100 characters). Appended to q unless the query already contains it. Off unless you send it.
pageinteger (1–10)Results page. Default: 1.
max_ageinteger (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
curl "https://api.104-236-84-15.sslip.io/v1/search?q=headless+browser+api&n=3" \
  -H "Authorization: Bearer $PAGETEER_API_KEY"
JavaScript (fetch)
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'));
Python (requests)
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

200 OK (snippets trimmed)
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.

Search response fields
NameTypeDescription
qualitycomplete | partial | unavailablecomplete: 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_countintegerNumber 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.
querystringWhat 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.
cachedbooleantrue when served from the search cache (0 credits).
cache_age_sintegerOnly when cached: seconds since the answer was fetched.
creditsintegerCredits 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.

Page content parameters
NameTypeDescription
include_contentbooleanFetch 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_resultsinteger (1–10)How many of the top results to fetch (never more than were returned). Default: 3.
markdown_modemain | fullAs on render: "full" keeps listing grids that "main" can drop. Default: main.
max_charsinteger (100–200000)Cut each page's markdown to this many characters (truncated: true says so).
content_timeout_msinteger (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_modeauto | http | browserFetch mode for the pages, as render mode. Default: auto.
unblocknone | stealth | residential | autoUnblocking tier for the pages, as on render. Default: none.
respect_robotsbooleanCheck each page's robots.txt first; disallowed pages fail with robots_disallowed (free). Default: false.
content_max_ageinteger (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_answerbooleanAdd a short answer from the fetched pages, with citations (+40 credits when returned). Default: false.
curl
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"
JavaScript (fetch)
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'));
Python (requests)
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"])
200 OK (trimmed)
{
  "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
}
Page content fields
NameTypeDescription
results[].content.statusok | failed | skippedok: fetched and charged; failed: the fetch failed (free); skipped: the time budget ended before it started (free).
results[].content.markdownstringThe page as markdown (status ok).
results[].content.http_statusintegerThe site's HTTP status. A 5xx is failed with error target_error.
results[].content.errorobject{ code, message }: timeout, target_blocked, captcha_required, login_required, robots_disallowed, target_rate_limited, forbidden_target, target_error, insufficient_credits, …
results[].content.creditsintegerWhat this page cost.
contentobject{ 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 operators OR, AND, NOT. The order of engines does not matter; n, page and country must match.
  • max_age sets how old an answer you accept: lower it for fresher results, raise it (up to 24 hours) to reuse older ones. max_age=0 always 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

Search errors
StatusCodeWhenChargedWhat to do
400invalid_requestA parameter is invalid: an unknown engine, a country with no known locale and no language, n over 50, …NoFix the request; retrying it unchanged fails again.
401invalid_api_keyNo key, an unknown key, or a revoked or expired key.NoCheck the Authorization header and the key on the API keys page.
402insufficient_creditsNot 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.NoAct on the reason at error.billing_url: add a card, update the payment method, or restart the subscription. Or wait for the 1st (UTC).
402spend_limit_reachedBillable usage is on and this month’s spend limit is reached (error.reason: spend_limit).NoRaise the spend limit at error.billing_url (the Billing page), or wait for the 1st (UTC).
403org_suspendedThe account is suspended.NoContact support.
429rate_limitedOver your requests per minute, or too many requests to one site across the platform right now.NoWait Retry-After seconds, then back off with jitter.
429concurrency_limitToo many of your requests are in flight at once.NoWait for one to finish (Retry-After: 1) or lower your parallelism.
500 / 502 / 503internal_errorThe search backend failed.NoRetry with backoff (honor Retry-After); quote x-request-id if it persists.
504timeoutThe search backend did not answer in time.NoRetry, or name fewer engines.