# SerpKite documentation (full) > All 46 pages of https://serpkite.com/docs concatenated as Markdown. Index: https://serpkite.com/llms.txt SerpKite v1 API: base URL https://api.serpkite.com, every route under /v1 (e.g. POST /v1/search), `Authorization: Bearer skt_live_…`, snake_case responses with a `request` / `results` / `meta` envelope, POST /v1/batches for half-price queued jobs, official SDKs `serpkite` (npm, PyPI) and github.com/serpkite/serpkite-go. --- Source: https://serpkite.com/docs # SerpKite documentation > Real-time Google search results for AI agents and apps, as JSON, compact JSON or Markdown. Start here for the quickstart, endpoint reference, guides and MCP setup. SerpKite is a Google search API built for AI agents: clean JSON or Markdown, official TypeScript/Python/Go SDKs, native LangChain, CrewAI and MCP, credits that never expire. You send a query, and you get back the live Google results page as structured JSON, as token-lean compact JSON, or as Markdown that you can hand straight to an LLM. Every response has the same envelope: `request` (what ran), `results` (the main list for every endpoint), endpoint extras such as `knowledge_graph` and `people_also_ask`, and `meta` (cost, cache, request ID). Every link is the resolved destination URL. There is also a Google Custom Search JSON API–compatible endpoint for teams moving off CSE before it shuts down on January 1, 2027. ## Make your first request cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","country":"us","language":"en"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine 2026", country: "us", language: "en" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine 2026", country="us", language="en") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "best espresso machine 2026", Country: "us", Language: "en"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` Install an SDK with `npm install serpkite`, `pip install serpkite` or `go get github.com/serpkite/serpkite-go`, or call the HTTP API directly. See [SDKs](https://serpkite.com/docs/sdks). New accounts get 1,000 free credits, plus 1,500 more when you link GitHub or Google, and 1,000 every month after that. No card required. [Get a key](https://app.serpkite.com/login?signup=1), then follow the [quickstart](https://serpkite.com/docs/quickstart). ## Start here - [Quickstart](https://serpkite.com/docs/quickstart): Get a key and run your first search in under five minutes. - [Authentication](https://serpkite.com/docs/authentication): Bearer tokens, query-string keys and keeping keys safe. - [Output formats](https://serpkite.com/docs/output-formats): JSON, compact and Markdown, plus fields projection. - [Credits and billing](https://serpkite.com/docs/credits-and-billing): What each call costs and what is never billed. ## Endpoints Every endpoint lives under `https://api.serpkite.com/v1` and takes a `POST` with a JSON object (or a `GET` with the same query parameters). The exception is the CSE-compatible `GET /customsearch/v1`. Credits are per results page of 10 unless noted. Unknown parameters are rejected with a `400` that names the right one, so typos never fall back to defaults silently (see [Strict validation](https://serpkite.com/docs/parameters#strict-validation)). | Endpoint | Method and path | Credits | Returns | | --- | --- | --- | --- | | [Google Search](https://serpkite.com/docs/endpoints/search) | `POST /v1/search` | 1 | `request`, `results`, `answer_box`, `knowledge_graph`, `people_also_ask`, `related_searches`, `top_stories`, `places`, `ads`, `meta` | | [Google News](https://serpkite.com/docs/endpoints/news) | `POST /v1/news` | 1 | `request`, `results`, `meta` | | [Google Images](https://serpkite.com/docs/endpoints/images) | `POST /v1/images` | 1 | `request`, `results`, `meta` | | [Google Videos](https://serpkite.com/docs/endpoints/videos) | `POST /v1/videos` | 1 | `request`, `results`, `meta` | | [Google Maps](https://serpkite.com/docs/endpoints/maps) | `POST /v1/maps` | 1 | `request`, `results`, `meta` | | [Google Places](https://serpkite.com/docs/endpoints/places) | `POST /v1/places` | 1 | `request`, `results`, `meta` | | [Google Reviews](https://serpkite.com/docs/endpoints/reviews) | `POST /v1/reviews` | 1 | `request`, `results`, `next_page_token`, `meta` | | [Google Shopping](https://serpkite.com/docs/endpoints/shopping) | `POST /v1/shopping` | 1 | `request`, `results`, `meta` | | [Google Scholar](https://serpkite.com/docs/endpoints/scholar) | `POST /v1/scholar` | 1 | `request`, `results`, `meta` | | [Google Patents](https://serpkite.com/docs/endpoints/patents) | `POST /v1/patents` | 1 | `request`, `results`, `meta` | | [Google Autocomplete](https://serpkite.com/docs/endpoints/autocomplete) | `POST /v1/autocomplete` | 0.5 | `request`, `results`, `meta` | | [Google Lens](https://serpkite.com/docs/endpoints/lens) | `POST /v1/lens` | 2 | `request`, `results`, `meta` | | [Webpage to Markdown](https://serpkite.com/docs/endpoints/webpage) | `POST /v1/webpage` | 1 | `request`, `url`, `status_code`, `markdown`, `text`, `metadata`, `meta` | | [Custom Search (CSE-compatible)](https://serpkite.com/docs/endpoints/customsearch) | `GET /customsearch/v1` | 1 | `kind`, `searchInformation`, `items`, `queries` | To run many queries at half price, queue them with [`POST /v1/batches`](https://serpkite.com/docs/batch). Two free endpoints help with bookkeeping: [`GET /v1/account`](https://serpkite.com/docs/endpoints/account) returns your balance and this month's usage, and [`GET /v1/batches/{id}`](https://serpkite.com/docs/endpoints/batches) returns the status and result of a batch job. ## Build with it - [MCP server](https://serpkite.com/docs/mcp): Remote MCP at api.serpkite.com/v1/mcp for Claude, Cursor, VS Code and ChatGPT. - [Official SDKs](https://serpkite.com/docs/sdks): TypeScript, Python and Go clients, plus LangChain and CrewAI tools. - [Tool calling](https://serpkite.com/docs/guides/agents-tool-calling): Ready-made tool definitions for OpenAI and Anthropic models. - [RAG pipeline](https://serpkite.com/docs/guides/rag-pipeline): Search, fetch the top pages as Markdown, and ground an answer. - [Rank tracking](https://serpkite.com/docs/guides/rank-tracking): Top-100 depth, the batch lane and webhooks for SEO tools. ## Moving off Google Custom Search Point `https://www.googleapis.com/customsearch/v1` at `https://api.serpkite.com/customsearch/v1` and keep your existing CSE client. See [Migrate from Google CSE](https://serpkite.com/docs/guides/migrate-from-google-cse). ## Machine-readable docs Every page here has a Markdown twin: add `.md` to the URL (for example [/docs/quickstart.md](https://serpkite.com/docs/quickstart.md)), or use the **Copy page** button above. The whole documentation is also available as [llms.txt](https://serpkite.com/llms.txt) and [llms-full.txt](https://serpkite.com/llms-full.txt), and the API contract as an [OpenAPI 3.1 spec](https://serpkite.com/openapi/serp-api.yaml). --- Source: https://serpkite.com/docs/quickstart # Quickstart > Create an API key, install the SDK (or use curl), run your first Google search with SerpKite, read the response and check your credit balance. Takes about five minutes. This guide takes you from zero to a working search call. Use one of the official SDKs (TypeScript, Python or Go), or any HTTP client: the API is plain JSON over HTTPS. 1. ### Create an account and a key Sign in at [app.serpkite.com](https://app.serpkite.com/login?signup=1) with your email, GitHub or Google. New accounts start with 1,000 free credits (2,500 once you link GitHub or Google). No card is needed. Open **API keys**, click **Create key**, give it a name such as `local-dev`, and copy the secret. It starts with `skt_live_` and is shown only once. 2. ### Store the key in an environment variable Keep the key out of source code. The SDKs and every example in these docs read it from `SERPKITE_API_KEY`: ```bash export SERPKITE_API_KEY="skt_live_..." ``` 3. ### Install an SDK (optional) The SDKs handle auth, retries, timeouts and typed responses. Skip this step if you prefer curl or your own HTTP client. ```bash npm install serpkite # TypeScript / JavaScript (Node 18+, Bun, Deno, edge) pip install serpkite # Python 3.9+ (sync and async clients) go get github.com/serpkite/serpkite-go # Go ``` LangChain and CrewAI tools are available too. See [SDKs](https://serpkite.com/docs/sdks). 4. ### Run a search `POST /v1/search` takes a JSON object. Only `q` is required. `country` sets the country and `language` the interface language. cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","country":"us","language":"en"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine 2026", country: "us", language: "en" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine 2026", country="us", language="en") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "best espresso machine 2026", Country: "us", Language: "en"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` 5. ### Read the response Every endpoint returns the same envelope. `request` echoes the normalised request, `results` holds the ten blue links with resolved destination URLs and a canonical `domain`, and `meta` tells you what the call cost and whether the page parsed cleanly. All keys are snake_case. 200 OK · illustrative: ```json { "request": { "endpoint": "search", "engine": "google", "q": "best espresso machine 2026", "country": "us", "language": "en", "num": 10, "page": 1, "device": "desktop", "autocorrect": true }, "results": [ { "position": 1, "title": "The Best Espresso Machines of 2026, Tested and Reviewed", "link": "https://www.example.com/best-espresso-machines", "domain": "example.com", "displayed_link": "https://www.example.com › best-espresso-machines", "snippet": "We pulled more than 1,200 shots on 42 machines to find the best espresso makers for every budget, from beginner-friendly to prosumer.", "date": "Sep 12, 2026", "sitelinks": [ { "title": "Best budget pick", "link": "https://www.example.com/best-espresso-machines#budget" }, { "title": "Best dual boiler", "link": "https://www.example.com/best-espresso-machines#dual-boiler" } ] }, { "position": 2, "title": "Espresso Machine Buying Guide (2026)", "link": "https://coffee.example.org/guides/espresso", "domain": "coffee.example.org", "displayed_link": "https://coffee.example.org › guides › espresso", "snippet": "Single boiler, heat exchanger or dual boiler? What the specs mean and which features are worth paying for." }, { "position": 3, "title": "r/espresso: What machine would you buy in 2026?", "link": "https://www.reddit.com/r/espresso/comments/abc123/", "domain": "reddit.com", "displayed_link": "https://www.reddit.com › r › espresso", "snippet": "Discussion thread with 480 comments comparing entry-level and prosumer machines." } ], "people_also_ask": [ { "question": "What is the #1 rated espresso machine?", "snippet": "Reviewers most often rank dual-boiler machines with PID control at the top…", "link": "https://www.example.com/best-espresso-machines" }, { "question": "Is a $500 espresso machine worth it?", "snippet": "For daily drinkers, a mid-range machine usually pays for itself within a year…", "link": "https://coffee.example.org/guides/espresso" } ], "related_searches": [ { "query": "best espresso machine under $500" }, { "query": "best espresso machine for beginners" }, { "query": "dual boiler vs heat exchanger" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 942, "parse_quality": "ok", "resolved_urls": true } } ``` ## Check what it cost Every billed response carries headers with the cost and your remaining balance, so you never need a separate call to track spend. `meta.credits_used` repeats the cost in the body: Response headers: ```http HTTP/2 200 content-type: application/json x-request-id: req_01J8ZK4M6Q2V7 x-credits-used: 1 x-credits-remaining: 61499 x-cost-usd: 0.0008 x-cache: MISS x-latency-ms: 942 x-tokens-estimate: 1184 ``` A regular search costs 1 credit per page of 10 results. Failed, empty and blocked requests are refunded automatically and show `X-Credits-Used: 0`. See [Credits and billing](https://serpkite.com/docs/credits-and-billing) and [Response headers](https://serpkite.com/docs/response-headers). You can also ask for the balance directly. `GET /v1/account` is free: cURL: ```bash curl "https://api.serpkite.com/v1/account" \ -H "Authorization: Bearer $SERPKITE_API_KEY" ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); const account = await sk.account(); console.log(account.balance, account.month.credits); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() account = sk.account() print(account.balance, account.month.credits) ``` Go: ```go c := serpkite.NewClient() account, err := c.Account(ctx) if err != nil { log.Fatal(err) } fmt.Println(account.Balance, account.Month.Credits) ``` ## Get fewer tokens for an LLM If the results go into a model's context, ask for Markdown. The same query drops from a large JSON document to a few hundred tokens of prose, with links kept. The SDKs return a plain string for `format: "markdown"`: cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","country":"us","format":"markdown"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine 2026", country: "us", format: "markdown" }); console.log(res); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine 2026", country="us", format="markdown") print(res) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.SearchMarkdown(ctx, serpkite.SearchParams{Q: "best espresso machine 2026", Country: "us"}) if err != nil { log.Fatal(err) } fmt.Println(res) } ``` format=markdown: ```markdown # best espresso machine 2026 ## Results 1. **The Best Espresso Machines of 2026, Tested and Reviewed** — example.com https://www.example.com/best-espresso-machines We pulled more than 1,200 shots on 42 machines to find the best espresso makers for every budget. 2. **Espresso Machine Buying Guide (2026)** — coffee.example.org https://coffee.example.org/guides/espresso Single boiler, heat exchanger or dual boiler? What the specs mean. 3. **r/espresso: What machine would you buy in 2026?** — reddit.com https://www.reddit.com/r/espresso/comments/abc123/ ## People also ask - **What is the #1 rated espresso machine?** Reviewers most often rank dual-boiler machines with PID control at the top… - **Is a $500 espresso machine worth it?** For daily drinkers, a mid-range machine usually pays for itself within a year… ## Related searches best espresso machine under $500 · best espresso machine for beginners · dual boiler vs heat exchanger ``` `format: "compact"` returns lean JSON instead, and `fields` projects only the parts you need (for example `results.title,results.link,knowledge_graph`). The `X-Tokens-Estimate` header tells you how big the body is. See [Output formats](https://serpkite.com/docs/output-formats). ## Handle errors Errors always have the same shape and are never billed: ```json { "error": { "code": "insufficient_credits", "message": "Your balance is 0 credits. Buy a pack at https://app.serpkite.com/billing.", "request_id": "req_01J8ZK7T2RX4B" } } ``` The SDKs raise it as a typed error (`SerpKiteError` in TypeScript and Python, `*serpkite.Error` in Go) with `status`, `code`, `message` and the request ID, and retry `429` and `5xx` for you. Retry `429` and `503` with backoff (honour `Retry-After`). Fix the request for `400`, the key for `401`, and your balance or limits for `402` and `403`. The full list is in [Errors](https://serpkite.com/docs/errors). Parameters are validated strictly: an unknown or misspelled parameter returns `400 invalid_request` and names the right one, instead of being ignored. 400 Bad Request: ```json { "error": { "code": "invalid_request", "message": "unknown parameter \"gl\": use country", "request_id": "req_01J8ZK9W3HC2D" } } ``` See [Strict validation](https://serpkite.com/docs/parameters#strict-validation). ## Next steps - [Common parameters](https://serpkite.com/docs/parameters): Country, language, location, device, time range and more. - [Search reference](https://serpkite.com/docs/endpoints/search): Every request parameter and response field of /v1/search. - [Official SDKs](https://serpkite.com/docs/sdks): TypeScript, Python and Go clients, LangChain and CrewAI tools. - [Use it from Claude or Cursor](https://serpkite.com/docs/mcp): Add the remote MCP server with one config snippet. - [Give an agent web search](https://serpkite.com/docs/guides/agents-tool-calling): Tool definitions for OpenAI and Anthropic function calling. --- Source: https://serpkite.com/docs/authentication # Authentication > Every SerpKite API call is authenticated with a secret API key, sent as a Bearer token in the Authorization header, or on GET requests as a query parameter. ## API keys SerpKite keys look like `skt_live_` followed by a random secret. You create them in the dashboard at [app.serpkite.com](https://app.serpkite.com) under **API keys**. The full secret is shown exactly once, when the key is created or rotated. We store only a SHA-256 hash of it, so nobody at SerpKite can read it back to you. If you lose a key, [rotate it](https://serpkite.com/docs/api-keys#rotate-a-key). The `skt_` prefix is deliberate: it doesn't collide with Stripe's `sk_` pattern, so secret scanners (GitHub push protection and similar) can tell a leaked SerpKite key apart from a Stripe one. An account can have several keys, each with its own name and optional monthly credit limit. See [API keys](https://serpkite.com/docs/api-keys). ## Sending the key Send the key as a Bearer token in the `Authorization` header. This works on every request, including the [MCP server](https://serpkite.com/docs/mcp): | Method | Example | Works on | | --- | --- | --- | | Bearer token (recommended) | `Authorization: Bearer skt_live_…` | All requests | | Query parameter | `?api_key=skt_live_…` | `GET` requests only | | Google's `key` parameter | `?key=skt_live_…` | [`GET /customsearch/v1`](https://serpkite.com/docs/endpoints/customsearch) only | Other headers are not read. A request without a valid `Authorization: Bearer` header (or `api_key` on a `GET`) gets `401 unauthorized`. ### Bearer token The official SDKs send the header for you and read the key from `SERPKITE_API_KEY` unless you pass one explicitly (`new SerpKite({ apiKey })`, `SerpKite(api_key=...)`, `serpkite.WithAPIKey(...)`). With curl or your own HTTP client, set the header yourself: cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","country":"us","language":"en"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine 2026", country: "us", language: "en" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine 2026", country="us", language="en") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "best espresso machine 2026", Country: "us", Language: "en"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` ### Query parameter (GET only) For `GET` requests you can put the key in the URL as `api_key`. This exists for tools that can only paste a URL (spreadsheets, no-code HTTP nodes). The [Custom Search–compatible endpoint](https://serpkite.com/docs/endpoints/customsearch) also accepts Google's `key=`, so existing CSE clients keep working. ```bash curl "https://api.serpkite.com/v1/search?q=best+espresso+machine+2026&country=us&api_key=$SERPKITE_API_KEY" ``` > **Prefer headers** > URLs end up in proxy logs, browser history and analytics tools. Use a header whenever your client allows it, and never put a key in a URL that a browser or a third party will see. ## Keep keys secret - Load the key from an environment variable or a secrets manager. All examples in these docs read `SERPKITE_API_KEY`. - Don't ship keys in mobile apps or public frontend bundles. The API sends `Access-Control-Allow-Origin: *` so that a browser playground with the user's own key works, but anyone who can read your JavaScript can read a key embedded in it. Call SerpKite from your backend instead. - Use one key per environment or service (`production`, `staging`, `n8n`) and give each a [monthly credit limit](https://serpkite.com/docs/api-keys#per-key-monthly-limit). A leaked key then has a bounded cost, and you can revoke it without touching the others. - If a key leaks, [rotate or revoke it](https://serpkite.com/docs/api-keys) in the dashboard. Revocation is effective within about a minute. ## Checking a key `GET /v1/account` is free and returns the balance, rate limit and this month's usage for the account behind the key. It is a cheap way to validate a key at startup: cURL: ```bash curl "https://api.serpkite.com/v1/account" \ -H "Authorization: Bearer $SERPKITE_API_KEY" ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); const account = await sk.account(); console.log(account.balance, account.month.credits); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() account = sk.account() print(account.balance, account.month.credits) ``` Go: ```go c := serpkite.NewClient() account, err := c.Account(ctx) if err != nil { log.Fatal(err) } fmt.Println(account.Balance, account.Month.Credits) ``` A missing, malformed or revoked key gets `401 unauthorized`: ```json { "error": { "code": "unauthorized", "message": "invalid or revoked API key", "request_id": "req_01J8ZK9P3WQ5N" } } ``` The SDKs raise this as an error you can catch: `SerpKiteError` in TypeScript and Python (with `status`, `code`, `message` and the request ID), `*serpkite.Error` in Go. Check for `code === "unauthorized"` rather than parsing the message. The dashboard itself (app.serpkite.com) uses a separate session cookie, not API keys. API keys only work against `api.serpkite.com`. ## Related - [API keys](https://serpkite.com/docs/api-keys): Create, limit, rotate and revoke keys. - [Errors](https://serpkite.com/docs/errors): Every error code and what to do about it. - [Rate limits](https://serpkite.com/docs/rate-limits): Requests per second per key, and how to back off. - [MCP server](https://serpkite.com/docs/mcp): Use your key as a Bearer token for the remote MCP server. --- Source: https://serpkite.com/docs/credits-and-billing # Credits and billing > SerpKite is prepaid. You buy credit packs that never expire, each call spends a known number of credits, and failed or empty calls are refunded automatically. ## How credits work A credit is the unit every call is priced in. One credit buys one Google results page (up to 10 organic results) from [/v1/search](https://serpkite.com/docs/endpoints/search) or any core vertical. Other endpoints and options cost more or less, as listed below. - **Prepaid, no subscription.** You buy a pack once and spend it down. There is no monthly fee and nothing renews. - **Credits never expire.** Packs stack: buy a Starter today and a Pro next year, and both balances add up. - **Decimal amounts.** Some calls cost half a credit, so balances and headers are decimal numbers such as `61499.5`. - **You only pay for results.** Failed, empty and blocked requests cost nothing. ## What each call costs | Endpoint or option | Credits | Notes | | --- | --- | --- | | `/v1/search`, `/v1/news`, `/v1/images`, `/v1/videos`, `/v1/maps`, `/v1/places`, `/v1/shopping`, `/v1/scholar`, `/v1/patents`, `/v1/webpage`, `/customsearch/v1` | 1 | Per results page (10 results). | | `num=100` depth bundle | 7 | Top 100 results in one call instead of 10 pages. | | `/v1/reviews` | 1 per 10 reviews | `num` up to 50. | | `/v1/autocomplete` | 0.5 | | | `/v1/lens` | 2 | | | `include_content=N` | +1 per fetched page | Up to 5 pages. | | `POST /v1/batches` | ×0.5 | Queued, delivered by poll or webhook. | | Cache hit (`max_age`) | ×0.5 | Only when a fresh-enough cached result exists. | | Failed, empty or blocked request | 0 | Reserved credits are refunded automatically. | | `GET /v1/account`, `GET /v1/batches/{id}`, `GET /v1/status` | 0 | | A few worked examples: | Request | Credits | | --- | --- | | `POST /v1/search` with `q` only | 1 | | `POST /v1/search` with `page: 3` | 1 (each page is a separate call) | | `POST /v1/search` with `num: 100` | 7 (instead of 10 for ten pages) | | `POST /v1/search` with `include_content: 3` | 1 + 3 = 4 | | One search queued with `POST /v1/batches` | 0.5 | | `POST /v1/search` with `max_age: 3600`, cache hit | 0.5 | | `POST /v1/reviews` with `num: 30`, 30 reviews returned | 3 (1 per 10 returned; 12 returned costs 2) | | `POST /v1/autocomplete` | 0.5 | The batch and cache discounts apply to the full price of the call. Half prices are rounded up to the nearest thousandth of a credit, so nothing ever rounds down to free. See [Pagination and depth](https://serpkite.com/docs/pagination-and-depth), [Caching](https://serpkite.com/docs/caching), [Batch requests](https://serpkite.com/docs/batch) and [Page content](https://serpkite.com/docs/include-content) for the details of each option. ## What is never billed - **Errors.** Any `4xx` or `5xx` response costs 0 credits, including `503 upstream_error`, `503 upstream_blocked` and `503 upstream_timeout`. See [Errors](https://serpkite.com/docs/errors). - **Empty results.** If Google returns no results for your query (`meta.parse_quality: "empty"`), the call is refunded. - **Failed page fetches.** With `include_content`, you pay only for the pages we actually fetched. Pages that time out or refuse the fetch are refunded. - **Bookkeeping endpoints.** [`GET /v1/account`](https://serpkite.com/docs/endpoints/account), [`GET /v1/batches/{id}`](https://serpkite.com/docs/endpoints/batches) and `GET /v1/status` are free. ### Reservation and refund When a call starts, SerpKite reserves its maximum possible cost from your balance. When the call finishes, it settles to the real cost and refunds the difference. If the call fails, the whole reservation is refunded. You can see this in two places: - The response headers: `X-Credits-Used` is the final cost (`0` for a refunded call) and `X-Credits-Remaining` is the balance after settlement. See [Response headers](https://serpkite.com/docs/response-headers). - The credit ledger in the dashboard (**Billing**). It is append-only: every purchase, grant, usage and refund is a row, and refunds are separate compensating rows rather than edits. Ledger reasons are `grant`, `purchase`, `usage` and `refund`. Usage is aggregated per day. If your balance is lower than the reservation, the call is rejected with `402 insufficient_credits` before anything runs. ## Free tier - **1,000 credits** when you sign up and verify your email. - **1,500 more** the first time you link a GitHub or Google account. - **1,000 credits every month** after that, for verified accounts. - **5 requests per second** per key. No card is required. Free and purchased credits share one balance. A daily cap may apply to free accounts; if you hit it you get `429 daily_limit_reached`, and buying any pack removes it. ## Credit packs | Pack | Price | Credits | Per 1,000 | Rate limit | | --- | --- | --- | --- | --- | | Free | $0 | 1,000 on signup, +1,500 for linking GitHub or Google, 1,000 every month | | 5 req/s | | Starter | $10 | 10,000 | $1.00 | 20 req/s | | Growth | $50 | 62,500 | $0.80 | 50 req/s | | Pro | $300 | 500,000 | $0.60 | 100 req/s | Buying a pack also raises the [rate limit](https://serpkite.com/docs/rate-limits) of all your keys to that pack's rate. The rate comes from the largest pack you have ever bought, so it doesn't drop when your balance runs down. Committed volume above these packs (from $0.18 per 1,000) is available on [Enterprise](https://serpkite.com/enterprise) plans. ## Paying Packs are one-time purchases through Paddle, our merchant of record. Paddle handles the checkout, card processing, VAT and sales tax, and issues the invoice. Listed prices include VAT or GST in the EU, UK and most other countries; in the US, Canada and India, sales tax is added on top at checkout. 1. ### Open Billing In the dashboard, go to **Billing** and pick a pack. 2. ### Check out You are taken to the Paddle checkout. Pay by card or any method Paddle offers in your country. 3. ### Credits land on your balance When Paddle confirms the payment, the credits are added to your balance, usually within seconds. The grant is idempotent: a retried webhook never credits you twice. Every paid order is listed under **Billing → Orders** with a link to the invoice PDF hosted by Paddle. Enterprise customers can pay by invoice. To top up automatically when the balance runs low, see [auto-recharge](https://serpkite.com/docs/spend-controls#auto-recharge). To cap how much you spend per month, see [Spend controls](https://serpkite.com/docs/spend-controls). ## Tracking spend - **Per request:** `meta.credits_used` in the body and the `X-Credits-Used`, `X-Credits-Remaining` and `X-Cost-USD` headers. `X-Cost-USD` is the dollar value of the credits used, at the per-credit price of the largest pack you have bought (`0` on a free account). - **Per account and key:** [`GET /v1/account`](https://serpkite.com/docs/endpoints/account) returns the balance and this month's credits and requests; the dashboard's **Usage** page breaks usage down by day, endpoint and key and exports CSV. - **Alerts:** email alerts for low balance and for a percentage of your monthly cap. See [Spend controls](https://serpkite.com/docs/spend-controls). ## Refunds of purchases Unused credits don't expire, so there is rarely a reason to refund a pack. If you bought a pack by mistake and haven't used any of its credits, email support@serpkite.com with the order ID from **Billing → Orders** within 14 days of purchase and we refund it through Paddle. Refunded credits are removed from your balance. Free credits and partly used packs are not refundable, except where the law requires it. Unused purchased credits are forfeited when you delete your account, so ask for a refund first. The full rules are in the [Refund Policy](https://serpkite.com/legal/refunds). --- Source: https://serpkite.com/docs/rate-limits # Rate limits > SerpKite limits requests per second per API key. Free keys get 5 per second; buying a pack raises every key to that pack's rate. Over the limit you get a 429 with Retry-After. ## Limits per plan Rate limits are **requests per second, per API key**. They protect the upstream and keep latency predictable; they are not a quota. Your total volume is bounded only by your credit balance and any [spend controls](https://serpkite.com/docs/spend-controls) you set. | Plan | Requests per second, per key | | --- | --- | | Free | 5 | | Starter | 20 | | Growth | 50 | | Pro | 100 | | Enterprise | Custom | Your rate is set by the **largest pack you have ever bought**, not by your current balance. Buy a Pro pack once and every key on the account stays at 100 requests per second, even after the credits are spent and you top up with Starter packs. [`GET /v1/account`](https://serpkite.com/docs/endpoints/account) returns the current value as `rate_limit_rps`. Because the limit is per key, two services with separate keys don't compete for the same budget. Keys on a [team](https://serpkite.com/docs/team) account all get the owner's rate. ## What counts as a request - Every call to a search endpoint counts as one request. - A [`POST /v1/batches`](https://serpkite.com/docs/batch) call counts **once**, however many requests it holds, so every plan can submit the full 100 per call. The jobs then run on the batch lane at their own pace, so batch is the way to push large volumes without managing concurrency yourself. What bounds a batch is the queue: a single account can have up to 10,000 jobs queued at once. - `GET /v1/account` and `GET /v1/batches/{id}` are free but still authenticated calls. Poll batch jobs every few seconds, not in a tight loop, or use a [webhook](https://serpkite.com/docs/webhooks) instead. - [MCP](https://serpkite.com/docs/mcp) tool calls count like the equivalent REST call. ## When you hit the limit Over the limit, the API answers `429 Too Many Requests` with the `rate_limited` code and a `Retry-After` header (seconds). Nothing is billed. ```http HTTP/2 429 content-type: application/json retry-after: 1 x-request-id: req_01J8ZKA1B2C3D {"error":{"code":"rate_limited","message":"rate limit exceeded","request_id":"req_01J8ZKA1B2C3D"}} ``` `Retry-After` is exposed through CORS, so browser code can read it too. Two other 429 codes are **not** about request rate, and retrying soon won't help: - `daily_limit_reached`: a free account hit its daily cap. Buying any pack removes it. - `rate_limited` with a message about queued batch jobs: you have too many batch jobs waiting. Let some finish first. ## Retrying well The official SDKs already do this: they honour `Retry-After` and retry `429` and `5xx` with exponential backoff (configurable with `maxRetries`, `max_retries` or `serpkite.WithMaxRetries`). With your own HTTP client, wait for `Retry-After`, then retry with exponential backoff and jitter. The same approach works for `503` (upstream errors, blocks and timeouts), which is also free and safe to retry. Python: ```python import os import random import time import requests API = "https://api.serpkite.com" HEADERS = {"Authorization": f"Bearer {os.environ['SERPKITE_API_KEY']}"} RETRY = {429, 500, 502, 503, 504} def search(body: dict, attempts: int = 5) -> dict: for i in range(attempts): res = requests.post(f"{API}/v1/search", json=body, headers=HEADERS, timeout=30) if res.status_code not in RETRY: res.raise_for_status() return res.json() code = res.json()["error"]["code"] if code == "daily_limit_reached": raise RuntimeError("free-tier daily limit reached") wait = float(res.headers.get("Retry-After", 0)) or min(2**i, 30) time.sleep(wait + random.random() / 2) res.raise_for_status() return res.json() ``` Node.js: ```javascript const RETRY = new Set([429, 500, 502, 503, 504]); export async function search(body, attempts = 5) { for (let i = 0; ; i++) { const res = await fetch("https://api.serpkite.com/v1/search", { method: "POST", headers: { Authorization: `Bearer ${process.env.SERPKITE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (res.ok) return res.json(); const { error } = await res.json(); if (!RETRY.has(res.status) || i + 1 >= attempts || error.code === "daily_limit_reached") { throw new Error(`${error.code}: ${error.message} (${error.request_id})`); } const wait = Number(res.headers.get("Retry-After")) || Math.min(2 ** i, 30); await new Promise((r) => setTimeout(r, wait * 1000 + Math.random() * 500)); } } ``` Go: ```go func search(ctx context.Context, body []byte) (*http.Response, error) { for i := 0; ; i++ { req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.serpkite.com/v1/search", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("SERPKITE_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { return nil, err } retry := res.StatusCode == 429 || res.StatusCode >= 500 if !retry || i >= 4 { return res, nil } res.Body.Close() wait, _ := strconv.Atoi(res.Header.Get("Retry-After")) if wait == 0 { wait = 1 << i } time.Sleep(time.Duration(wait)*time.Second + time.Duration(rand.Intn(500))*time.Millisecond) } } ``` ## Staying under the limit - **Cap concurrency** in your client. At a typical latency of about a second, a limit of N requests per second means roughly N requests in flight at once. - **Use the batch lane** for anything that doesn't need an answer right now. [`POST /v1/batches`](https://serpkite.com/docs/batch) takes up to 100 requests per call, costs half price, and you don't have to pace it. - **Use the cache** for repeated queries. A `max_age` hit is half price and returns faster. See [Caching](https://serpkite.com/docs/caching). - **Split workloads across keys.** Each key has its own per-second budget. Need more than 100 requests per second? [Talk to us about Enterprise](https://serpkite.com/enterprise). ## Playground limits The no-login demo on the [playground](https://serpkite.com/playground) doesn't use a key, so it is limited per IP address instead: at most 5 searches per minute and 20 per day. Past either limit it answers `429 rate_limited`. Playground searches are never billed and don't count against your key's limit. For anything beyond a quick look, [create a free key](https://app.serpkite.com/login?signup=1). ## Related - [Errors](https://serpkite.com/docs/errors): All error codes, and which ones to retry. - [Batch requests](https://serpkite.com/docs/batch): Up to 100 queries per call on the half-price batch lane. - [Credits and billing](https://serpkite.com/docs/credits-and-billing): Packs and the rate each one unlocks. - [Response headers](https://serpkite.com/docs/response-headers): Retry-After and the credit headers. --- Source: https://serpkite.com/docs/errors # Errors > Every SerpKite error uses the same JSON body with a stable code, a human-readable message and a request ID. Errors are never billed. Here is every code and what to do about it. ## Error shape Whenever a call fails, the HTTP status is `4xx` or `5xx` and the body looks like this: 402 Payment Required: ```json { "error": { "code": "insufficient_credits", "message": "Your balance is 0 credits. Buy a pack at https://app.serpkite.com/billing.", "request_id": "req_01J8ZK7T2RX4B" } } ``` | Field | Meaning | | --- | --- | | `error.code` | Stable, machine-readable code. Branch on this, not on the message. | | `error.message` | Human-readable explanation. It may change wording over time. | | `error.request_id` | The same value as the `X-Request-Id` header. Quote it when you contact support. | The shape is the same on every endpoint, including the [Custom Search–compatible endpoint](https://serpkite.com/docs/endpoints/customsearch) (which does not imitate Google's error format) and the dashboard API. **Errors are never billed.** A failed call has `X-Credits-Used: 0`, and any credits reserved for it are refunded. See [Credits and billing](https://serpkite.com/docs/credits-and-billing#what-is-never-billed). ## All error codes | Status | Code | Retry? | Meaning | | --- | --- | --- | --- | | 400 | `invalid_request` | No | A parameter is missing or invalid. | | 401 | `unauthorized` | No | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | No | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 403 | `key_limit_reached` | No | This key hit its monthly credit limit. | | 403 | `spend_cap_reached` | No | The account's monthly spend cap is reached. Raise it in the dashboard or wait for the next UTC month. | | 404 | `not_found` | No | The batch job does not exist, belongs to another account, or its result has expired. | | 429 | `rate_limited` | Yes, with backoff | Too many requests per second for your plan. Retry after the Retry-After header. | | 429 | `daily_limit_reached` | No | Free-tier daily limit reached (when one is configured). Buying any pack removes it. | | 500 | `internal` | Yes, with backoff | Unexpected server error. Not billed; retry with backoff and quote the request_id if it persists. | | 503 | `upstream_error` | Yes, with backoff | Google could not be fetched or parsed. Not billed; retry after Retry-After. | | 503 | `upstream_blocked` | Yes, with backoff | Google is rate limiting the upstream right now. Not billed; retry after Retry-After. | | 503 | `upstream_timeout` | Yes, with backoff | The search took too long. Not billed; retry after Retry-After. | | 503 | `unavailable` | Yes, with backoff | The endpoint is temporarily unavailable. Not billed; retry with backoff. | ## What to do ### Fix the request (400) `invalid_request` means a parameter is missing, unknown, out of range or of the wrong type. The message names the parameter, for example `page must be between 1 and 10`, `num > 10 (depth) requires page=1` or `include_content is only supported on /v1/search`. Retrying the same request will fail the same way. Check the [parameter reference](https://serpkite.com/docs/parameters) and the endpoint's page. Unknown parameters are rejected rather than ignored, and when the name is one other SERP APIs use, the message tells you the replacement: 400 Bad Request: ```json { "error": { "code": "invalid_request", "message": "unknown parameter \"gl\": use country", "request_id": "req_01J8ZK9W3HC2D" } } ``` A JSON array body is also a `400`: send one object per call, or queue many with [`POST /v1/batches`](https://serpkite.com/docs/batch). See [Strict validation](https://serpkite.com/docs/parameters#strict-validation). ### Fix the key (401) `unauthorized` means the key is missing, malformed or revoked. The message tells you which. Check that you send `Authorization: Bearer skt_live_…` (or `?api_key=` on a `GET`), and that the key still exists under **API keys** in the dashboard. See [Authentication](https://serpkite.com/docs/authentication). ### Add credits or raise a limit (402, 403) - `insufficient_credits` (402): the balance can't cover the call. [Buy a pack](https://app.serpkite.com/billing) or wait for the monthly free grant. Turn on [low-balance alerts or auto-recharge](https://serpkite.com/docs/spend-controls) so this doesn't surprise you in production. - `key_limit_reached` (403): this key used its monthly credit limit. Raise or clear the limit on the key, or use another key. Limits reset at the start of the UTC calendar month. See [API keys](https://serpkite.com/docs/api-keys#per-key-monthly-limit). - `spend_cap_reached` (403): the whole account hit its monthly spend cap. Raise it under **Settings** or wait for the next UTC month. See [Spend controls](https://serpkite.com/docs/spend-controls). ### Slow down (429) - `rate_limited`: too many requests per second for this key, or too many batch jobs queued. Wait for `Retry-After` seconds and retry with backoff. See [Rate limits](https://serpkite.com/docs/rate-limits). - `daily_limit_reached`: a free account used its daily allowance. Retrying today won't help; buying any pack removes the cap. - The no-login [playground](https://serpkite.com/playground) has its own per-IP limits (5 searches per minute, 20 per day) and answers `429 rate_limited` when you pass them. API keys are not affected. ### Retry later (5xx) These are on our side or Google's, are never billed, and are safe to retry with exponential backoff. Upstream failures come with a `Retry-After` header (a few seconds); wait at least that long: - `upstream_error` (503): Google's page couldn't be fetched or parsed. - `upstream_blocked` (503): Google is rate limiting our upstream at the moment. - `upstream_timeout` (503): the search took too long. `include_content` and `num: 100` calls take longer, so give them a generous client timeout. - `unavailable` (503): the endpoint is temporarily unavailable. - `internal` (500): unexpected error. If it persists, send us the `request_id`. `404 not_found` is only returned by [`GET /v1/batches/{id}`](https://serpkite.com/docs/endpoints/batches): the job ID is wrong, belongs to another account, or its result expired. ## Errors in batch requests [`POST /v1/batches`](https://serpkite.com/docs/batch) returns `202` with one entry per request, in order. An entry that couldn't be queued (for example an invalid parameter) is an error object instead of a batch job: ```json { "batches": [ { "id": "0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "status": "queued", "endpoint": "/v1/search", "poll_url": "https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90" }, { "error": { "code": "invalid_request", "message": "q is required", "request_id": "req_01J8ZKB7M2:1" } } ] } ``` Only queued jobs are billed. Check each entry for an `error` key rather than relying on the HTTP status. Errors that apply to the whole call (a bad key, a rate limit, more than 100 requests) come back as a single error object with the matching status. A job that fails later has `status: "failed"` and an `error` object when you poll it, and is refunded. ## Handling errors in code The SDKs turn every error body into a typed exception with the same fields, and retry `429` and `5xx` for you (`maxRetries` / `max_retries` / `WithMaxRetries`): TypeScript: ```ts import { SerpKite, SerpKiteError } from "serpkite"; const sk = new SerpKite(); try { const res = await sk.search({ q: "best espresso machine 2026" }); console.log(res.results.length); } catch (err) { if (err instanceof SerpKiteError) { if (["insufficient_credits", "key_limit_reached", "spend_cap_reached"].includes(err.code)) { // alert a human: money or limits } throw new Error(`${err.status} ${err.code}: ${err.message} (${err.requestId})`); } throw err; } ``` Python: ```python from serpkite import SerpKite, SerpKiteError sk = SerpKite() try: res = sk.search("best espresso machine 2026") except SerpKiteError as err: if err.code in ("insufficient_credits", "key_limit_reached", "spend_cap_reached"): ... # alert a human: money or limits raise RuntimeError(f"{err.status} {err.code}: {err.message} ({err.request_id})") ``` Go: ```go res, err := c.Search(ctx, serpkite.SearchParams{Q: "best espresso machine 2026"}) var apiErr *serpkite.Error if errors.As(err, &apiErr) { switch apiErr.Code { case "insufficient_credits", "key_limit_reached", "spend_cap_reached": // alert a human: money or limits } return fmt.Errorf("%d %s: %s (%s)", apiErr.Status, apiErr.Code, apiErr.Message, apiErr.RequestID) } ``` cURL: ```bash curl -sS https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026"}' | jq '.error // empty' ``` With a raw HTTP client, check the status code and branch on `error.code`: retry `rate_limited`, `upstream_error`, `upstream_blocked`, `upstream_timeout` and `unavailable` with backoff, and surface everything else. A retry helper with backoff is in [Rate limits](https://serpkite.com/docs/rate-limits#retrying-well). ## Empty results are not errors A query that Google answers with no results returns `200` with an empty `results` array and `meta.parse_quality: "empty"`, and it is not billed. `parse_quality: "partial"` means some parts of the page could not be parsed; the parts that were are returned. Neither is an error. See [Response headers](https://serpkite.com/docs/response-headers#body-meta). ## Getting help Email support@serpkite.com with the `request_id` and the time of the request. We can find the request from its ID alone: we don't log query text, so the ID is how we trace a call. Service status is at [status.serpkite.com](https://status.serpkite.com). --- Source: https://serpkite.com/docs/response-headers # Response headers > Every billed SerpKite response tells you what it cost, your remaining balance, whether it came from cache, how long it took and roughly how many LLM tokens it contains. ## The headers POST /v1/search · 200 OK: ```http HTTP/2 200 content-type: application/json x-request-id: req_01J8ZK4M6Q2V7 x-credits-used: 1 x-credits-remaining: 61499 x-cost-usd: 0.0008 x-cache: MISS x-latency-ms: 942 x-tokens-estimate: 1184 ``` | Header | Meaning | | --- | --- | | `X-Request-Id` | Unique ID for this request. Quote it to support. | | `X-Credits-Used` | Credits charged for this request (0 if not billed). | | `X-Credits-Remaining` | Your balance after this request. | | `X-Cost-USD` | Effective cost of this request at your average pack price. | | `X-Cache` | HIT if served from cache (max_age), otherwise MISS. | | `X-Latency-Ms` | Server-side time to produce the response. | | `X-Tokens-Estimate` | Approximate LLM tokens in the response body. | Three more headers appear in specific cases: | Header | When | Meaning | | --- | --- | --- | | `Retry-After` | `429` responses | Seconds to wait before retrying. See [Rate limits](https://serpkite.com/docs/rate-limits). | | `Content-Type` | Always | `application/json`, or `text/markdown; charset=utf-8` with `format: "markdown"`. | Values are plain numbers. Credits are decimals (`0.5`, `61499.5`), `X-Cost-USD` is a decimal dollar amount, `X-Latency-Ms` and `X-Tokens-Estimate` are integers. ## What each one is for ### Cost and balance `X-Credits-Used` is the final cost of this call after any refund, so it is `0` for errors, empty results and failed page fetches. `X-Credits-Remaining` is your account balance right after the call. Together they let you track spend without ever calling [`/v1/account`](https://serpkite.com/docs/endpoints/account). `X-Cost-USD` converts the credits used to dollars at the per-credit price of the largest pack you have bought. On a free account it is `0`. It is a convenience for per-request cost attribution (for example, per customer or per agent run), not an invoice. ### Cache `X-Cache: HIT` means the result came from the cache because you sent `max_age` and a fresh-enough copy existed; the call cost half price. `MISS` means a live fetch. The body's `meta.cached` and `meta.cached_at` say the same thing. See [Caching](https://serpkite.com/docs/caching). ### Latency `X-Latency-Ms` is the time SerpKite spent producing the response, from receiving the request to writing the body. It excludes network time between you and us. ### Token estimate `X-Tokens-Estimate` is the approximate size of the response body in LLM tokens, computed as the number of characters divided by four. It's a quick way to compare `format: "json"`, `"compact"` and `"markdown"`, or to decide whether a result fits in a context window, without running a tokenizer. Real counts vary by model and language. See [Output formats](https://serpkite.com/docs/output-formats). ### Request ID `X-Request-Id` identifies this call. It is also in the body as `meta.request_id` (or `error.request_id` on errors) and in the dashboard's request log. Include it in bug reports. ## Reading headers in code The official SDKs parse the body for you; `res.meta.credits_used` (`res.Meta.CreditsUsed` in Go) carries the cost and `meta.request_id` the request ID. To read the raw headers, use any HTTP client: cURL: ```bash curl -sS -D - -o /dev/null https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026"}' | grep -i '^x-' ``` Python: ```python import os import httpx res = httpx.post( "https://api.serpkite.com/v1/search", headers={"Authorization": f"Bearer {os.environ['SERPKITE_API_KEY']}"}, json={"q": "best espresso machine 2026"}, timeout=30, ) used = float(res.headers["X-Credits-Used"]) left = float(res.headers["X-Credits-Remaining"]) print(f"{used} credits, {left} left, cache {res.headers['X-Cache']}, ~{res.headers['X-Tokens-Estimate']} tokens") ``` Node.js: ```javascript const res = await fetch("https://api.serpkite.com/v1/search", { method: "POST", headers: { Authorization: `Bearer ${process.env.SERPKITE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ q: "best espresso machine 2026" }), }); const h = res.headers; console.log({ used: Number(h.get("X-Credits-Used")), remaining: Number(h.get("X-Credits-Remaining")), costUsd: Number(h.get("X-Cost-USD")), cache: h.get("X-Cache"), tokens: Number(h.get("X-Tokens-Estimate")), requestId: h.get("X-Request-Id"), }); ``` Go: ```go used, _ := strconv.ParseFloat(res.Header.Get("X-Credits-Used"), 64) left, _ := strconv.ParseFloat(res.Header.Get("X-Credits-Remaining"), 64) fmt.Printf("%v credits, %v left, request %s\n", used, left, res.Header.Get("X-Request-Id")) ``` ## Browsers and CORS The API sends `Access-Control-Allow-Origin: *` (without credentials) and exposes all of the headers above, including `Retry-After`, through `Access-Control-Expose-Headers`. Browser code can read them with `response.headers.get()`. Keep in mind that a key used from a browser is visible to anyone using that page; see [Authentication](https://serpkite.com/docs/authentication#keep-keys-secret). ## Body meta The JSON body repeats the essentials in `meta`, which is handy when you only store the body, and adds parse information that has no header: | Field | Type | Meaning | | --- | --- | --- | | `request_id` | string | Same as `X-Request-Id`. | | `credits_used` | number | Same as `X-Credits-Used`. | | `engine` | string | The provider that answered, e.g. `google` or `brave`. See [Search providers](https://serpkite.com/docs/providers). | | `route` | array | Provider attempts behind a fresh result, in order: `{provider, outcome, ms}`. Absent on cache hits. | | `cached` | boolean | `true` on a cache hit. | | `cached_at` | string or null | When the cached copy was fetched (ISO 8601). | | `resolved_urls` | boolean | `true` when every result link is the resolved destination, never a Google redirect. | | `parse_quality` | string | `ok`, `partial` (some blocks couldn't be parsed) or `empty` (no results; not billed). | | `latency_ms` | integer | Same as `X-Latency-Ms`. | With `format: "markdown"` the body is plain Markdown and has no `meta`; rely on the headers there. With `fields`, `meta` and `request` are always kept. ## Related - [Credits and billing](https://serpkite.com/docs/credits-and-billing): What each call costs and what is refunded. - [Output formats](https://serpkite.com/docs/output-formats): Use X-Tokens-Estimate to pick a format. - [Caching](https://serpkite.com/docs/caching): max_age, X-Cache and half-price hits. - [Errors](https://serpkite.com/docs/errors): Error bodies carry request_id too. --- Source: https://serpkite.com/docs/parameters # Common parameters > Every request parameter the SerpKite search endpoints accept, with types, defaults and allowed values, grouped by what they control. All search endpoints under `/v1` share one parameter set, with snake_case names. You send it as a JSON object on `POST`, or as a query string on `GET` with the same names. Only `q` is required. Each endpoint's reference page lists exactly the parameters it accepts; for example [`/v1/reviews`](https://serpkite.com/docs/endpoints/reviews) takes `place_id` (or `cid`, `fid`) instead of `q`, and [`/v1/lens`](https://serpkite.com/docs/endpoints/lens) and [`/v1/webpage`](https://serpkite.com/docs/endpoints/webpage) take `url`. ## All parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `location` | string | | Canonical location for local results, e.g. "Austin, Texas, United States". Overrides country for geo. | | `uule` | string | | Google-encoded location string. Use instead of location if you already have it. | | `ll` | string | | Maps only: viewport as "@lat,lng,zoom", e.g. "@52.52,13.40,14z". | | `num` | integer | `10` | Results per call. 10 per page; 100 fetches the top 100 as a depth bundle for 7 credits instead of 10. One of: `10`, `20`, `30`, `50`, `100`. | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | | `time` | string | | Restrict to recent results. Shorthand for tbs=qdr:*. One of: `hour`, `day`, `week`, `month`, `year`. | | `tbs` | string | | Raw Google tbs filter, e.g. qdr:w or cdr:1,cd_min:… | | `device` | string | `desktop` | Which SERP layout to fetch. One of: `desktop`, `mobile`. | | `safe` | string | `off` | SafeSearch filtering. One of: `off`, `active`. | | `autocorrect` | boolean | `true` | Let Google correct misspelled queries. Set false to search the exact text. | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `include_content` | integer | `0` | Also fetch the top N result pages (0–5) as Markdown. +1 credit per page. | | `ads` | boolean | `false` | Include sponsored results in ads. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | | `engine` | string or array | `google` | Which search providers may answer. google is Google only (SerpKite still fails over across its own proxy pools); auto falls back to other providers when Google is blocked or times out; consensus (search only) asks several independent indexes in parallel, merges the results by URL, ranks them by agreement and lists each result's sources, at the sum of one page per provider that returned results; a provider name or a list (e.g. google,brave) restricts the request to those. meta.engine names the provider that answered. One of: `google`, `auto`, `consensus`, `brave`, `bing`, `yahoo`, `duckduckgo`, `mojeek`, `wikipedia`. | | `place_id` | string | | Reviews: Google place ID (from maps/places results). Or pass cid or fid instead. | | `cid` | string | | Reviews: Google customer ID of the place. | | `fid` | string | | Reviews: Google feature ID of the place. | | `sort` | string | `most_relevant` | Reviews: sort order. One of: `most_relevant`, `newest`, `highest_rating`, `lowest_rating`. | | `page_token` | string | | Reviews: next_page_token from the previous page, unchanged (bound to the place and sort). Paging reaches the first 100 reviews per sort order. | | `url` | string | | Lens: image URL. Webpage: page URL to fetch. | | `include_html` | boolean | `false` | Webpage: also return the raw HTML. | | `domain` | string | | Rank: domain to find, e.g. example.com (subdomains match). | `ll` (the map viewport) applies only to [/v1/maps](https://serpkite.com/docs/endpoints/maps). `engine` defaults to `google` (Google only); `auto`, a single provider or a list allows fallback engines on search, news, images and videos. See [Search providers](https://serpkite.com/docs/providers). ## Sending parameters The two requests below are equivalent: POST (JSON): ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"coffee grinder","country":"de","language":"de","num":20,"time":"month"}' ``` GET (query string): ```bash curl "https://api.serpkite.com/v1/search?q=coffee+grinder&country=de&language=de&num=20&time=month" \ -H "Authorization: Bearer $SERPKITE_API_KEY" ``` Numbers and booleans are accepted either as JSON numbers and booleans or as strings (`"num": "20"`, `"autocorrect": "false"`), so query strings and loosely typed clients both work. Invalid values return `400 invalid_request` with a message naming the parameter, and are never billed. The body must be a single JSON object. To run many queries at once, queue them with [`POST /v1/batches`](https://serpkite.com/docs/batch) (up to 100 per call, at half price). ## Strict validation Unknown parameters are rejected, never silently ignored, so a typo can't quietly fall back to a default (a misspelled `country` would otherwise search from the US). The API answers `400 invalid_request`, names the parameter and, when it recognises a name used by other SERP APIs, tells you the replacement: 400 Bad Request: ```json { "error": { "code": "invalid_request", "message": "unknown parameter \"gl\": use country", "request_id": "req_01J8ZK9W3HC2D" } } ``` | If you send | Use instead | | --- | --- | | `gl` | `country` | | `hl` | `language` | | `placeId` | `place_id` | | `sortBy` | `sort` | | `nextPageToken` | `page_token` | | `mode` | [`POST /v1/batches`](https://serpkite.com/docs/batch) | | `type` | The endpoint path, e.g. `/v1/news` instead of `"type": "news"` | A JSON array body is rejected the same way (`body must be a JSON object; to run many queries use POST /v1/batches`). None of these errors is billed. ## Query `q` is the search query, 1 to 2,048 characters. Google operators work as they do on google.com: `site:`, `-exclude`, `"exact phrase"`, `filetype:`, `intitle:` and so on. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | ## Location and language `country` picks the country Google searches from and `language` the interface language. `location` narrows results to a city or region, which matters for local packs, maps and anything with local intent. `uule` is the pre-encoded form of a location if you already have one. See [Localization](https://serpkite.com/docs/localization) for the full list of codes and how they interact. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `location` | string | | Canonical location for local results, e.g. "Austin, Texas, United States". Overrides country for geo. | | `uule` | string | | Google-encoded location string. Use instead of location if you already have it. | | `ll` | string | | Maps only: viewport as "@lat,lng,zoom", e.g. "@52.52,13.40,14z". | ## Paging and depth `page` walks through result pages, one credit each. `num` above 10 fetches deeper results in one call on /v1/search and /v1/news, and `num=100` is a bundle of 10 pages for 7 credits. See [Pagination and depth](https://serpkite.com/docs/pagination-and-depth). | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `num` | integer | `10` | Results per call. 10 per page; 100 fetches the top 100 as a depth bundle for 7 credits instead of 10. One of: `10`, `20`, `30`, `50`, `100`. | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | ## Filters `time` is a friendly shorthand for recency. `tbs` passes Google's raw filter string through when you need something `time` doesn't cover, like a custom date range. If you send both, `tbs` wins. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `time` | string | | Restrict to recent results. Shorthand for tbs=qdr:*. One of: `hour`, `day`, `week`, `month`, `year`. | | `tbs` | string | | Raw Google tbs filter, e.g. qdr:w or cdr:1,cd_min:… | | `device` | string | `desktop` | Which SERP layout to fetch. One of: `desktop`, `mobile`. | | `safe` | string | `off` | SafeSearch filtering. One of: `off`, `active`. | | `autocorrect` | boolean | `true` | Let Google correct misspelled queries. Set false to search the exact text. | | `time` | Equivalent `tbs` | | --- | --- | | `hour` | `qdr:h` | | `day` | `qdr:d` | | `week` | `qdr:w` | | `month` | `qdr:m` | | `year` | `qdr:y` | A custom date range uses `tbs` directly, for example `cdr:1,cd_min:1/1/2026,cd_max:3/31/2026`. ## Output These parameters change the shape and size of the response, not the search itself. See [Output formats](https://serpkite.com/docs/output-formats) and [Page content](https://serpkite.com/docs/include-content). | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `include_content` | integer | `0` | Also fetch the top N result pages (0–5) as Markdown. +1 credit per page. | | `ads` | boolean | `false` | Include sponsored results in ads. | ## Cost and delivery `max_age` lets you accept a recent cached result for half the credits. For work that doesn't need an answer right now, send the same request bodies to [`POST /v1/batches`](https://serpkite.com/docs/batch), also at half price, and get the results by polling or [webhook](https://serpkite.com/docs/webhooks). See [Caching](https://serpkite.com/docs/caching). | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | | `engine` | string or array | `google` | Which search providers may answer. google is Google only (SerpKite still fails over across its own proxy pools); auto falls back to other providers when Google is blocked or times out; consensus (search only) asks several independent indexes in parallel, merges the results by URL, ranks them by agreement and lists each result's sources, at the sum of one page per provider that returned results; a provider name or a list (e.g. google,brave) restricts the request to those. meta.engine names the provider that answered. One of: `google`, `auto`, `consensus`, `brave`, `bing`, `yahoo`, `duckduckgo`, `mojeek`, `wikipedia`. | ## Reviews, Lens and Webpage These endpoints take an identifier or URL instead of `q`. Reviews page with a cursor: pass `next_page_token` from the previous response as `page_token`. See [Pagination and depth](https://serpkite.com/docs/pagination-and-depth#paging-through-reviews). | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `place_id` | string | | Reviews: Google place ID (from maps/places results). Or pass cid or fid instead. | | `cid` | string | | Reviews: Google customer ID of the place. | | `fid` | string | | Reviews: Google feature ID of the place. | | `sort` | string | `most_relevant` | Reviews: sort order. One of: `most_relevant`, `newest`, `highest_rating`, `lowest_rating`. | | `page_token` | string | | Reviews: next_page_token from the previous page, unchanged (bound to the place and sort). Paging reaches the first 100 reviews per sort order. | | `url` | string | | Lens: image URL. Webpage: page URL to fetch. | | `include_html` | boolean | `false` | Webpage: also return the raw HTML. | ## Which endpoint takes what Each endpoint page lists exactly the parameters it supports: | Endpoint | Method and path | Credits | Returns | | --- | --- | --- | --- | | [Google Search](https://serpkite.com/docs/endpoints/search) | `POST /v1/search` | 1 | `request`, `results`, `answer_box`, `knowledge_graph`, `people_also_ask`, `related_searches`, `top_stories`, `places`, `ads`, `meta` | | [Google News](https://serpkite.com/docs/endpoints/news) | `POST /v1/news` | 1 | `request`, `results`, `meta` | | [Google Images](https://serpkite.com/docs/endpoints/images) | `POST /v1/images` | 1 | `request`, `results`, `meta` | | [Google Videos](https://serpkite.com/docs/endpoints/videos) | `POST /v1/videos` | 1 | `request`, `results`, `meta` | | [Google Maps](https://serpkite.com/docs/endpoints/maps) | `POST /v1/maps` | 1 | `request`, `results`, `meta` | | [Google Places](https://serpkite.com/docs/endpoints/places) | `POST /v1/places` | 1 | `request`, `results`, `meta` | | [Google Reviews](https://serpkite.com/docs/endpoints/reviews) | `POST /v1/reviews` | 1 | `request`, `results`, `next_page_token`, `meta` | | [Google Shopping](https://serpkite.com/docs/endpoints/shopping) | `POST /v1/shopping` | 1 | `request`, `results`, `meta` | | [Google Scholar](https://serpkite.com/docs/endpoints/scholar) | `POST /v1/scholar` | 1 | `request`, `results`, `meta` | | [Google Patents](https://serpkite.com/docs/endpoints/patents) | `POST /v1/patents` | 1 | `request`, `results`, `meta` | | [Google Autocomplete](https://serpkite.com/docs/endpoints/autocomplete) | `POST /v1/autocomplete` | 0.5 | `request`, `results`, `meta` | | [Google Lens](https://serpkite.com/docs/endpoints/lens) | `POST /v1/lens` | 2 | `request`, `results`, `meta` | | [Webpage to Markdown](https://serpkite.com/docs/endpoints/webpage) | `POST /v1/webpage` | 1 | `request`, `url`, `status_code`, `markdown`, `text`, `metadata`, `meta` | | [Custom Search (CSE-compatible)](https://serpkite.com/docs/endpoints/customsearch) | `GET /customsearch/v1` | 1 | `kind`, `searchInformation`, `items`, `queries` | --- Source: https://serpkite.com/docs/output-formats # Output formats > Get Google results as full JSON, token-lean compact JSON or LLM-ready Markdown, and cut them down further with fields projection. X-Tokens-Estimate tells you the size. Every search endpoint can answer in three formats. They contain the same search, just packaged for different consumers, and they cost the same credits. | `format` | Content type | Best for | | --- | --- | --- | | `json` (default) | `application/json` | Apps and pipelines that parse specific fields. The full `request` / `results` / `meta` envelope. | | `compact` | `application/json` | Agents that want structure but few tokens. The same `results`, trimmed: short snippets, no thumbnails, positions or tracking data. | | `markdown` (alias `md`) | `text/markdown` | Putting results straight into an LLM prompt or a tool result. | > **JSON-only endpoints** > `/v1/autocomplete` and `/v1/lens` always return JSON; their responses are already small. `format: "compact"` still works there. ## JSON The default. Every endpoint returns the same envelope, with snake_case keys throughout: - `request`: the normalised request that ran, with defaults filled in (`endpoint`, `engine`, `q`, `country`, `language`, `num`, `page`, `device`…). - `results`: the endpoint's main list. Organic results on /v1/search, articles on /v1/news, places on /v1/maps, reviews on /v1/reviews, suggestions on /v1/autocomplete, and so on. [/v1/webpage](https://serpkite.com/docs/endpoints/webpage) is the only endpoint without `results`; it returns `url`, `markdown` and `metadata`. - Endpoint extras. On /v1/search: `related_searches` (always present), and `answer_box`, `knowledge_graph`, `people_also_ask`, `top_stories`, `places` and `ads` when Google shows them. - `meta`: `request_id`, `credits_used`, `cached`, `engine` (the [provider](https://serpkite.com/docs/providers) that answered), `route`, `latency_ms`, `parse_quality` and `resolved_urls`. Every `link` is the resolved destination URL and every result carries a canonical `domain`. format=json · illustrative: ```json { "request": { "endpoint": "search", "engine": "google", "q": "best espresso machine 2026", "country": "us", "language": "en", "num": 10, "page": 1, "device": "desktop", "autocorrect": true }, "results": [ { "position": 1, "title": "The Best Espresso Machines of 2026, Tested and Reviewed", "link": "https://www.example.com/best-espresso-machines", "domain": "example.com", "displayed_link": "https://www.example.com › best-espresso-machines", "snippet": "We pulled more than 1,200 shots on 42 machines to find the best espresso makers for every budget, from beginner-friendly to prosumer.", "date": "Sep 12, 2026", "sitelinks": [ { "title": "Best budget pick", "link": "https://www.example.com/best-espresso-machines#budget" }, { "title": "Best dual boiler", "link": "https://www.example.com/best-espresso-machines#dual-boiler" } ] }, { "position": 2, "title": "Espresso Machine Buying Guide (2026)", "link": "https://coffee.example.org/guides/espresso", "domain": "coffee.example.org", "displayed_link": "https://coffee.example.org › guides › espresso", "snippet": "Single boiler, heat exchanger or dual boiler? What the specs mean and which features are worth paying for." }, { "position": 3, "title": "r/espresso: What machine would you buy in 2026?", "link": "https://www.reddit.com/r/espresso/comments/abc123/", "domain": "reddit.com", "displayed_link": "https://www.reddit.com › r › espresso", "snippet": "Discussion thread with 480 comments comparing entry-level and prosumer machines." } ], "people_also_ask": [ { "question": "What is the #1 rated espresso machine?", "snippet": "Reviewers most often rank dual-boiler machines with PID control at the top…", "link": "https://www.example.com/best-espresso-machines" }, { "question": "Is a $500 espresso machine worth it?", "snippet": "For daily drinkers, a mid-range machine usually pays for itself within a year…", "link": "https://coffee.example.org/guides/espresso" } ], "related_searches": [ { "query": "best espresso machine under $500" }, { "query": "best espresso machine for beginners" }, { "query": "dual boiler vs heat exchanger" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 942, "parse_quality": "ok", "resolved_urls": true } } ``` The fields of each endpoint are documented on its page, for example [Search](https://serpkite.com/docs/endpoints/search#response). ## Compact `format: "compact"` returns a token-lean JSON object with only what a model needs to reason about the results. It keeps the same `results` key and snake_case names, shortens snippets, drops empty values, and removes positions, thumbnails, sitelinks and tracking data. `meta` stays, and `fields` works on compact output too. cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","country":"us","format":"compact"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine 2026", country: "us", format: "compact" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine 2026", country="us", format="compact") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "best espresso machine 2026", Country: "us", Format: "compact"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` format=compact · illustrative: ```json { "results": [ { "title": "The Best Espresso Machines of 2026, Tested and Reviewed", "link": "https://www.example.com/best-espresso-machines", "snippet": "We pulled more than 1,200 shots on 42 machines to find the best espresso makers for every budget, from beginner-friendly to prosumer.", "date": "Sep 12, 2026" }, { "title": "Espresso Machine Buying Guide (2026)", "link": "https://coffee.example.org/guides/espresso", "snippet": "Single boiler, heat exchanger or dual boiler? What the specs mean and which features are worth paying for." }, { "title": "r/espresso: What machine would you buy in 2026?", "link": "https://www.reddit.com/r/espresso/comments/abc123/", "snippet": "Discussion thread with 480 comments comparing entry-level and prosumer machines." } ], "people_also_ask": [ "What is the #1 rated espresso machine?", "Is a $500 espresso machine worth it?" ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 942, "parse_quality": "ok", "resolved_urls": true } } ``` Compact keys per endpoint: | Endpoint | Keys | | --- | --- | | `/v1/search`, `/v1/scholar`, `/v1/patents`, `/v1/lens` | `results[]` (`title`, `link`, `snippet`, `date`, `content`, `cited_by`, `year`), plus on /v1/search `answer`, `knowledge_graph` (`title`, `type`, `description`, `website`), `people_also_ask[]` (question strings), `top_stories[]` | | `/v1/news` | `results[]` (`title`, `link`, `source`, `date`, `snippet`) | | `/v1/images` | `results[]` (`title`, `image_url`, `link`) | | `/v1/videos` | `results[]` (`title`, `link`, `channel`, `duration`, `date`) | | `/v1/maps`, `/v1/places` | `results[]` (`title`, `address`, `rating`, `rating_count`, `phone`, `website`, `type`) | | `/v1/reviews` | `results[]` (`rating`, `date`, `text`) | | `/v1/shopping` | `results[]` (`title`, `price`, `source`, `link`, `rating`) | | `/v1/autocomplete` | `results[]` (suggestion strings) | | `/v1/webpage` | `url`, `title`, `markdown` | Keys other than `results` only appear when Google returned something for them, so check for presence rather than `null`. ## Markdown `format: "markdown"` renders the results page as a Markdown document with `Content-Type: text/markdown`. Headings separate the sections (results, People also ask, related searches) and links are kept. It is usually the cheapest way to give a model the whole page. cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","country":"us","format":"markdown"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine 2026", country: "us", format: "markdown" }); console.log(res); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine 2026", country="us", format="markdown") print(res) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.SearchMarkdown(ctx, serpkite.SearchParams{Q: "best espresso machine 2026", Country: "us"}) if err != nil { log.Fatal(err) } fmt.Println(res) } ``` format=markdown · illustrative: ```markdown # best espresso machine 2026 ## Results 1. **The Best Espresso Machines of 2026, Tested and Reviewed** — example.com https://www.example.com/best-espresso-machines We pulled more than 1,200 shots on 42 machines to find the best espresso makers for every budget. 2. **Espresso Machine Buying Guide (2026)** — coffee.example.org https://coffee.example.org/guides/espresso Single boiler, heat exchanger or dual boiler? What the specs mean. 3. **r/espresso: What machine would you buy in 2026?** — reddit.com https://www.reddit.com/r/espresso/comments/abc123/ ## People also ask - **What is the #1 rated espresso machine?** Reviewers most often rank dual-boiler machines with PID control at the top… - **Is a $500 espresso machine worth it?** For daily drinkers, a mid-range machine usually pays for itself within a year… ## Related searches best espresso machine under $500 · best espresso machine for beginners · dual boiler vs heat exchanger ``` The body is plain text. The SDKs return it as a string (`SearchMarkdown` in Go); with a raw HTTP client read it with `res.text()` (Node) or `res.text` (Python), not as JSON. Errors are still JSON with the usual [error shape](https://serpkite.com/docs/errors), so check the status code first. ## Fields projection `fields` keeps only the parts of the response you ask for. It takes a comma-separated list of dot paths. Arrays are traversed element by element, so `results.title` keeps the title of every result. cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","fields":"results.title,results.link,knowledge_graph.title"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine 2026", fields: "results.title,results.link,knowledge_graph.title" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine 2026", fields="results.title,results.link,knowledge_graph.title") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "best espresso machine 2026", Fields: "results.title,results.link,knowledge_graph.title"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` ```json { "request": { "endpoint": "search", "engine": "google", "q": "best espresso machine 2026", "country": "us", "language": "en", "num": 10, "page": 1 }, "results": [ { "title": "The Best Espresso Machines of 2026, Tested and Reviewed", "link": "https://www.example.com/best-espresso-machines" }, { "title": "Espresso Machine Buying Guide (2026)", "link": "https://coffee.example.org/guides/espresso" } ], "knowledge_graph": { "title": "Espresso machine" }, "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google" } } ``` Rules: - Up to 50 paths per request, each at most 4 levels deep (`results.sitelinks.title` is fine). - `request` and `meta` are always kept, so you can still read the request ID and cost. - Paths that don't exist in the response are ignored. - With `format: "markdown"`, top-level names in `fields` select which sections are rendered, e.g. `fields: "answer_box,results"` drops People also ask and related searches. - Invalid paths (empty segments, too deep, too many) return `400 invalid_request`. ## Measuring size: X-Tokens-Estimate Every response carries `X-Tokens-Estimate`, an approximation of the LLM tokens in the body (characters divided by four). Use it to compare formats for your queries, or to decide how many results fit in a context window before you read the body. ```bash curl -s -o /dev/null -D - https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","format":"markdown"}' | grep -i x-tokens-estimate ``` It is an estimate, not your model's tokenizer; real counts vary by model and language. For a side-by-side comparison on your own query, try the [SERP token counter](https://serpkite.com/tools/serp-token-counter). ## Choosing a format - Building an app or storing results: `json`, with `fields` to drop what you don't read. - Agent tool results where the model reasons over the page: `markdown`. - Agent tool results where your code post-processes before the model sees them: `compact`. - Feeding full pages, not just snippets: add [`include_content`](https://serpkite.com/docs/include-content). Related: [Common parameters](https://serpkite.com/docs/parameters), [Tool calling for agents](https://serpkite.com/docs/guides/agents-tool-calling), [Response headers](https://serpkite.com/docs/response-headers). --- Source: https://serpkite.com/docs/localization # Localization > Search Google as a user in a specific country, language and city with country, language, location, uule and ll, including code tables for common countries and languages. Google's results depend heavily on where the searcher is and which language they use. SerpKite exposes the same levers Google uses, with plain names. | Parameter | Controls | Example | | --- | --- | --- | | `country` | Country the search runs from | `de` | | `language` | Interface language | `de` | | `location` | City or region, as free text | `"Munich, Bavaria, Germany"` | | `uule` | A pre-encoded Google location | `w+CAIQICI...` | | `ll` | Exact map viewport ([/v1/maps](https://serpkite.com/docs/endpoints/maps) only) | `"@48.137,11.575,14z"` | Defaults are `country: "us"` and `language: "en"`. The normalised values come back in `request.country` and `request.language` on every response. Without `location`, results are country-level. ## Country and language `country` is a two-letter ISO 3166-1 alpha-2 country code (case-insensitive). `language` is a language code (`en`, `de`) or a language plus region (`pt-BR`, `zh-TW`). They are independent: `country: "ch"` with `language: "fr"` is a French-speaking user in Switzerland. cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"beste Kaffeemühle","country":"de","language":"de"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "beste Kaffeemühle", country: "de", language: "de" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("beste Kaffeemühle", country="de", language="de") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "beste Kaffeemühle", Country: "de", Language: "de"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` Set both to match your target market. Mixing them (German query, `country: "us"`) is valid but returns what an American user searching in German sees, which is rarely what you want for rank tracking. Invalid codes return `400 invalid_request` ("country must be a country code", "language must be a language code") and are not billed. ## City-level location `location` is a free-text place name up to 256 characters. Use the canonical form "City, Region, Country", e.g. `"Austin, Texas, United States"`. It matters most for queries with local intent ("dentist", "coffee near me"), for [/v1/maps](https://serpkite.com/docs/endpoints/maps), [/v1/places](https://serpkite.com/docs/endpoints/places) and [/v1/shopping](https://serpkite.com/docs/endpoints/shopping), and for local packs inside [/v1/search](https://serpkite.com/docs/endpoints/search). cURL: ```bash curl https://api.serpkite.com/v1/places \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"dentist","location":"Austin, Texas, United States","country":"us"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.places({ q: "dentist", location: "Austin, Texas, United States", country: "us" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.places("dentist", location="Austin, Texas, United States", country="us") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Places(ctx, serpkite.SearchParams{Q: "dentist", Location: "Austin, Texas, United States", Country: "us"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` `location` overrides `country` for geography. Keep `country` set to the same country anyway, so the Google domain and language defaults line up. ## uule `uule` is Google's own encoded location string. If you already store locations in that form (many rank trackers do), pass it directly; it overrides `location`. Maximum length is 512 characters. Most users should use `location` and let SerpKite handle the encoding. ## Map viewport (ll) On [/v1/maps](https://serpkite.com/docs/endpoints/maps), `ll` pins the search to an exact viewport: `@latitude,longitude,zoomz`. Higher zoom means a smaller area. cURL: ```bash curl https://api.serpkite.com/v1/maps \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"coffee roasters","ll":"@52.52,13.405,14z"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.maps({ q: "coffee roasters", ll: "@52.52,13.405,14z" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.maps("coffee roasters", ll="@52.52,13.405,14z") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Maps(ctx, serpkite.SearchParams{Q: "coffee roasters", LL: "@52.52,13.405,14z"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` ## Country codes Common values of `country`. Any valid ISO 3166-1 alpha-2 code works, not only the ones listed. | `country` | Country | | --- | --- | | `us` | United States | | `gb` | United Kingdom | | `ca` | Canada | | `au` | Australia | | `in` | India | | `de` | Germany | | `fr` | France | | `es` | Spain | | `it` | Italy | | `nl` | Netherlands | | `be` | Belgium | | `ch` | Switzerland | | `at` | Austria | | `se` | Sweden | | `no` | Norway | | `dk` | Denmark | | `fi` | Finland | | `pl` | Poland | | `cz` | Czechia | | `pt` | Portugal | | `ie` | Ireland | | `br` | Brazil | | `mx` | Mexico | | `ar` | Argentina | | `co` | Colombia | | `cl` | Chile | | `jp` | Japan | | `kr` | South Korea | | `sg` | Singapore | | `id` | Indonesia | | `my` | Malaysia | | `ph` | Philippines | | `th` | Thailand | | `vn` | Vietnam | | `tr` | Türkiye | | `ae` | United Arab Emirates | | `sa` | Saudi Arabia | | `za` | South Africa | | `ng` | Nigeria | | `nz` | New Zealand | ## Language codes Common values of `language`. Other language codes Google supports work too. | `language` | Language | | --- | --- | | `en` | English | | `de` | German | | `fr` | French | | `es` | Spanish | | `it` | Italian | | `pt` | Portuguese | | `pt-BR` | Portuguese (Brazil) | | `nl` | Dutch | | `sv` | Swedish | | `da` | Danish | | `no` | Norwegian | | `fi` | Finnish | | `pl` | Polish | | `cs` | Czech | | `tr` | Turkish | | `ja` | Japanese | | `ko` | Korean | | `zh-CN` | Chinese (Simplified) | | `zh-TW` | Chinese (Traditional) | | `hi` | Hindi | | `id` | Indonesian | | `th` | Thai | | `vi` | Vietnamese | | `ar` | Arabic | ## Tips - For rank tracking, fix `country`, `language`, `location` and `device` per keyword and never change them between runs, or positions stop being comparable. See [Rank tracking](https://serpkite.com/docs/guides/rank-tracking). - Mobile and desktop layouts differ; set `device: "mobile"` if your users are on phones. See [Common parameters](https://serpkite.com/docs/parameters). - Localized results are cached per location, so [`max_age`](https://serpkite.com/docs/caching) hits only when all of these parameters match. --- Source: https://serpkite.com/docs/pagination-and-depth # Pagination and depth > Page through Google results with page, or fetch up to the top 100 in one call with num. num=100 is a depth bundle that costs 7 credits instead of 10. Google serves results ten at a time. SerpKite gives you two ways to go deeper: request pages one by one with `page`, or ask for up to 100 results in a single call with `num`. ## page `page` selects the results page, from 1 (default) to 10. Each page is a separate Google fetch and a separate credit. cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"espresso grinder","country":"us","page":2}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "espresso grinder", country: "us", page: 2 }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("espresso grinder", country="us", page=2) print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "espresso grinder", Country: "us", Page: 2}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` `page` works on every paged endpoint (search, news, images, videos, maps, places, shopping, scholar, patents). ## num and the depth bundle On [/v1/search](https://serpkite.com/docs/endpoints/search) and [/v1/news](https://serpkite.com/docs/endpoints/news), `num` above 10 fetches several pages in one request and merges them into one `results` list. SerpKite fetches `ceil(num / 10)` pages and bills one credit per page that returned results, capped at 7: | `num` | Pages fetched | Credits (at most) | | --- | --- | --- | | 1–10 | 1 | 1 | | 20 | 2 | 2 | | 30 | 3 | 3 | | 50 | 5 | 5 | | 70 | 7 | 7 | | 100 | 10 | **7** | So the top 100 costs 7 credits instead of the 10 you would pay with ten `page` calls, and it comes back in one response with consistent positions. Pages that come back empty are free: a long-tail query with results on only two pages costs 2 credits, whatever `num` you asked for. A cache hit with `max_age` costs half of what the cached result cost. cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"espresso grinder","country":"us","num":100}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "espresso grinder", country: "us", num: 100 }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("espresso grinder", country="us", num=100) print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "espresso grinder", Country: "us", Num: 100}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` Rules: - `num` must be between 1 and 100. Values up to 10 behave like 10. - `num` above 10 requires `page: 1` (or no `page`). Combining them returns `400 invalid_request`. - On other endpoints `num` above 10 is treated as 10; use `page` to go deeper. [/v1/reviews](https://serpkite.com/docs/endpoints/reviews) is the exception: `num` up to 50 reviews, billed 1 credit per 10 reviews actually returned. - Google sometimes returns fewer than `num` results for a query. You are billed for the pages actually fetched, and a page that comes back empty is refunded. > **Batch it for rank tracking** > The depth bundle combines with the batch lane: `num: 100` in a [`POST /v1/batches`](https://serpkite.com/docs/batch) request costs 3.5 credits for the top 100. See [Batch requests](https://serpkite.com/docs/batch) and [Rank tracking](https://serpkite.com/docs/guides/rank-tracking). ## Custom Search pagination The CSE-compatible [`/customsearch/v1`](https://serpkite.com/docs/endpoints/customsearch) keeps Google's CSE paging: `start` is the index of the first result (1, 11, 21 … up to 91) and `num` is 1 to 10 per call. Each call is one credit. ```bash curl "https://api.serpkite.com/customsearch/v1?q=asyncio&start=11&num=10" \ -H "Authorization: Bearer $SERPKITE_API_KEY" ``` ## Paging through reviews [/v1/reviews](https://serpkite.com/docs/endpoints/reviews) uses a cursor instead of page numbers. Each response includes `next_page_token`; send it back as `page_token` to get the next batch, and stop when it is absent. - The token is opaque and bound to the place and `sort` it came from. Send it unchanged with the same `place_id` (or `cid`/`fid`) and `sort`; anything else is a `400`. - Paging reaches the first 100 reviews of a place per sort order. The page that reaches review 100 has no `next_page_token`. - A page can hold fewer than `num` reviews when Google stops loading early. Its `next_page_token` resumes right after it, and you're billed for the reviews returned. Python: ```python from serpkite import SerpKite sk = SerpKite() reviews, token = [], None while True: res = sk.reviews(place_id="ChIJLU7jZClu5kcR4PcOOO6p3I0", sort="newest", num=50, page_token=token) reviews += res.results token = res.next_page_token if not token: # absent on the last page (at most 100 reviews) break print(len(reviews), "reviews") ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); const reviews = []; let page_token: string | undefined; do { const res = await sk.reviews({ place_id: "ChIJLU7jZClu5kcR4PcOOO6p3I0", sort: "newest", num: 50, page_token }); reviews.push(...res.results); page_token = res.next_page_token; } while (page_token); // absent on the last page (at most 100 reviews) console.log(reviews.length, "reviews"); ``` cURL: ```bash curl https://api.serpkite.com/v1/reviews \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"place_id":"ChIJLU7jZClu5kcR4PcOOO6p3I0","sort":"newest","num":50,"page_token":""}' ``` Related: [Credits and billing](https://serpkite.com/docs/credits-and-billing), [Common parameters](https://serpkite.com/docs/parameters). --- Source: https://serpkite.com/docs/caching # Caching > Accept a recent cached result with max_age and pay half the credits on a hit. How freshness, cache keys, X-Cache and meta.cached work. By default every request fetches Google live. If a slightly older result is fine for your use case, set `max_age` to the oldest result you accept, in seconds. When SerpKite has a matching result that fresh, it returns it immediately for **half the credits**. Otherwise it fetches live at the normal price and stores the result for the next caller. cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","country":"us","max_age":3600}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine 2026", country: "us", max_age: 3600 }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine 2026", country="us", max_age=3600) print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "best espresso machine 2026", Country: "us", MaxAge: 3600}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` ## How it works 1. You send a request with `max_age: 3600` (one hour). 2. SerpKite looks up a result for the same normalised request: same endpoint, query, `country`, `language`, location, device, page, filters and so on. 3. If one exists and is younger than `max_age`, it is returned with `X-Cache: HIT` and costs 0.5× the usual credits. 4. If not, or if it is older, the search runs live at full price with `X-Cache: MISS`, and the fresh result replaces the cached one. Without `max_age` (or with `max_age: 0`) you always get a live result and it is never served from cache. The maximum is 2,592,000 seconds (30 days). ## Pricing A cache hit costs half of what the same live request would, rounded up to the next 0.001 credit so it is never free: | Request | Live | Cache hit | | --- | --- | --- | | `/v1/search` (1 page) | 1 | 0.5 | | `/v1/search` with `num: 100` | 7 | 3.5 | | `/v1/autocomplete` | 0.5 | 0.25 | | `/v1/lens` | 2 | 1 | The `X-Credits-Used` header and `meta.credits_used` show the actual charge. ## Telling a hit from a miss - The `X-Cache` response header is `HIT` or `MISS`. - In JSON, `meta.cached` is `true` on a hit and `meta.cached_at` holds the time the result was fetched (ISO 8601). ```json "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 0.5, "cached": true, "cached_at": "2026-09-29T07:12:44Z", "engine": "google", "parse_quality": "ok" } ``` ## How long results stay cached `max_age` is the freshness you accept, but results also expire on their own. Current defaults: | Endpoint | Kept for up to | | --- | --- | | Most endpoints | 6 hours | | `/v1/news` | 30 minutes | | `/v1/webpage` | 1 hour | | `/v1/autocomplete` | 24 hours | So `max_age: 86400` on `/v1/search` behaves like "anything from the last 6 hours". These retention times may change; `max_age` is the contract. ## Privacy Every successful live result is written to the cache, whether or not the request sent `max_age`: `max_age` only decides whether *you* are served a cached copy. Entries are stored in memory under a SHA-256 hash of the normalised request, so the query text is never used as a key, and they expire automatically after the times above. Your identity and API key are not part of the entry. Raw HTML (`include_html`) is never cached. See [Privacy and data retention](https://serpkite.com/docs/privacy-and-data-retention). ## When to use it - **Agents and chatbots:** popular questions repeat. `max_age: 3600` cuts cost on repeated queries without users noticing. - **Dashboards that refresh often:** use a `max_age` a little shorter than your refresh interval. - **Rank tracking:** usually leave it off, or set it to your tracking window, so each run reflects a fresh SERP. The [batch lane](https://serpkite.com/docs/batch) is the cheaper lever there. - **News monitoring:** keep `max_age` short; news caching is capped at 30 minutes anyway. `max_age` is also honoured inside [batch requests](https://serpkite.com/docs/batch) (`POST /v1/batches`). Related: [Credits and billing](https://serpkite.com/docs/credits-and-billing), [Response headers](https://serpkite.com/docs/response-headers). --- Source: https://serpkite.com/docs/providers # Search providers > Requests are answered by Google by default. With engine=auto (or a per-key setting) SerpKite can fall back to Brave, Bing, Yahoo or DuckDuckGo when Google is unavailable, and always labels the engine that answered. SerpKite is a Google SERP API, and **every request is answered by Google unless you say otherwise**. Behind each Google request, SerpKite already retries across its own proxy pools and fetch methods when one of them is blocked or slow. That failover is always on, costs you nothing extra, and never changes which search engine you get. For workloads where *some* answer beats no answer (agents, chatbots, research tools), you can also let SerpKite fall back to other search engines when Google is blocked or times out. This is opt-in, per request or per key, and the response always says which engine produced it. ## The `engine` parameter `engine` is accepted by [/v1/search](https://serpkite.com/docs/endpoints/search), [/v1/news](https://serpkite.com/docs/endpoints/news), [/v1/images](https://serpkite.com/docs/endpoints/images) and [/v1/videos](https://serpkite.com/docs/endpoints/videos): | Value | What happens | | --- | --- | | `google` (default) | Google only. SerpKite fails over across its own proxy pools, never to another engine. | | `auto` | Google first; if Google is blocked or times out, the next enabled fallback engine that serves this vertical answers. | | `consensus` (search only) | Several independent engines in parallel, merged and ranked by agreement. See [Consensus mode](#consensus-mode). | | One provider, e.g. `brave` | That provider only. | | A list, e.g. `["google", "brave"]` | Only those providers, in SerpKite's route order. In a query string, send `engine=google,brave`. | cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","country":"us","engine":"auto"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine 2026", country: "us", engine: "auto" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine 2026", country="us", engine="auto") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "best espresso machine 2026", Country: "us", Engine: serpkite.Engines("auto")}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` Parameter errors (a bad `country`, an unknown parameter) never trigger a fallback: they return `400` straight away. `auto` and `consensus` can't be combined with other values, and naming only providers that don't serve the endpoint (or aren't enabled) returns `400 invalid_request` with a link to this page. ## Which providers serve which verticals | Provider | `engine` | `/v1/search` | `/v1/news` | `/v1/images` | `/v1/videos` | Other verticals | Notes | | --- | --- | --- | --- | --- | --- | --- | --- | | **Google** (default) | `google` | Yes | Yes | Yes | Yes | Yes, all | The default for every request. Every vertical and all Google-only blocks. | | Brave Search | `brave` | Yes | Yes | Yes | Yes | No | Its own independent index. | | Bing | `bing` | Yes | Yes | Yes | Yes | No | Microsoft's index. | | Yahoo | `yahoo` | Yes | Yes | No | No | No | Results largely come from Bing's index. | | DuckDuckGo | `duckduckgo` | Yes | Yes | Yes | Yes | No | Web results largely from Bing's index; news, images and videos from its own feeds. | | Mojeek | `mojeek` | Yes | No | No | No | No | Web results only, from its own independent index. | | Wikipedia | `wikipedia` | Yes | No | No | No | No | The best matching article, from Wikipedia's official API. A source for engine=consensus (or by name), never an auto fallback. | Google is always available. The fallback engines are enabled per deployment, so `engine=auto` only uses the ones that are currently switched on, and the route can change as SerpKite adds or pauses providers. Verticals not listed for a fallback engine (maps, places, reviews, shopping, scholar, patents, autocomplete, lens) are answered by Google only: `engine=auto` simply uses Google there, and naming a fallback engine returns `400`. ## Honest labelling: `meta.engine` and `meta.route` A result is never relabelled. `meta.engine` names the provider that actually produced it, and `meta.route` lists each provider attempt behind a fresh result, in order, with its outcome and duration: ```json "request": { "endpoint": "search", "engine": "auto", "q": "best espresso machine 2026", "country": "us" }, "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "brave", "route": [ { "provider": "google", "outcome": "blocked", "ms": 2140 }, { "provider": "brave", "outcome": "ok", "ms": 612 } ], "parse_quality": "ok", "latency_ms": 2790 } ``` - `request.engine` echoes what you asked for (`google`, `auto`, one provider or a list). - `meta.route` outcomes include `ok`, `partial`, `empty`, `blocked`, `timeout` and `error`. SerpKite's own proxy pools are internal and never appear in the route. - Cache hits have no `meta.route`. The cache never mixes providers: a Google-only request is only ever served a cached Google result. - With `format: "markdown"` there is no `meta`; SerpKite's [response headers](https://serpkite.com/docs/response-headers) still carry the request ID and credits. If you need Google specifically (rank tracking, SEO audits, anything that compares against google.com), keep the default and check `meta.engine` if you're unsure how a key is configured. ## What a fallback result lacks Fallback engines map into the same response shape (`results` with `title`, `link`, `snippet`…), so your parser keeps working, but their pages simply don't have Google's extras: - No knowledge graph, answer box, People Also Ask, top stories or local pack unless that engine has an equivalent block. Fields a provider doesn't have are absent or empty. - Ranking, snippets and result counts come from that engine's own index, so positions are not comparable with Google's. - `parse_quality` is judged per provider. ## Billing follows the provider You pay for the provider that answered, and only for that one. Every provider currently costs the same credits as Google for the same request, so a fallback never costs more. Failed, blocked and empty attempts along the route are free, just like a failed Google request: the request is settled once, at the price of the answer you get. `X-Credits-Used` and `meta.credits_used` show the actual charge. See [Credits and billing](https://serpkite.com/docs/credits-and-billing). ## Consensus mode `engine: "consensus"` on [/v1/search](https://serpkite.com/docs/endpoints/search) asks several independent search indexes at once and merges what they return, like a metasearch engine: - SerpKite starts up to three providers from its route, one per index. Yahoo and DuckDuckGo serve Bing's results, so at most one of the three is Bing-backed. Wikipedia's official API (the best matching article) can be one of them; it is only ever a consensus source, never an `auto` fallback. - Results are deduplicated by URL (ignoring `www.`, a trailing `/` and tracking parameters such as `utm_*`) and ranked by how many providers returned them. Among equally agreed results, Wikipedia articles and results whose title and snippet contain your query's words come first, then the best position any provider gave. - Each result has `sources`, the providers that returned it. `meta.engine` is `"consensus"` and `meta.route` lists every provider started. - It stops as soon as `num` unique results are in hand, or after a few seconds with whatever has arrived. Providers still running are cancelled (`canceled` in the route, `timeout` for the deadline) and ones never started cost nothing. ```json "results": [ { "position": 1, "title": "The Best Espresso Machines", "link": "https://www.seriouseats.com/best-espresso-machines-5185482", "domain": "seriouseats.com", "sources": ["brave", "bing"] }, { "position": 2, "title": "Espresso machine - Wikipedia", "link": "https://en.wikipedia.org/wiki/Espresso_machine", "domain": "en.wikipedia.org", "sources": ["wikipedia"] } ], "meta": { "engine": "consensus", "credits_used": 3, "route": [ { "provider": "brave", "outcome": "ok", "ms": 640 }, { "provider": "wikipedia", "outcome": "ok", "ms": 410 }, { "provider": "bing", "outcome": "ok", "ms": 1180 } ] } ``` **Billing:** one page at each provider that returned results, added up (so up to three times a single search). Providers that failed, came back empty, were blocked or never started are free, and a cache hit (`max_age`) costs half. The whole reservation counts against your spend cap and key limit. Consensus answers are cached separately: they are never served to a single-engine request, or the reverse. ## Per-key setting In the dashboard, [API keys](https://serpkite.com/docs/api-keys) have a switch: **Allow fallback to other search engines** (off by default). With it on, requests from that key that don't send `engine` behave as `engine=auto`. An explicit `engine` always wins, so `engine: "google"` keeps a single request Google-only even on a key with fallback on. This is useful when you can't change the calling code, for example an agent framework or an [MCP](https://serpkite.com/docs/mcp) client that doesn't expose the parameter. ## Always Google Some surfaces stay on Google regardless of `engine` or the key setting: - The [Custom Search compatible endpoint](https://serpkite.com/docs/endpoints/customsearch) (`GET /customsearch/v1`), because it promises Google Custom Search results. - Every vertical without a fallback engine in the table above. Related: [Common parameters](https://serpkite.com/docs/parameters), [Errors](https://serpkite.com/docs/errors), [Caching](https://serpkite.com/docs/caching). --- Source: https://serpkite.com/docs/batch # Batch requests > Queue up to 100 requests per call with POST /v1/batches at half the credits. Poll GET /v1/batches/{id}, wait with the SDKs, or receive a signed webhook. When you don't need the answer while you wait (rank tracking, nightly enrichment, dataset building), send the work to the batch lane. `POST /v1/batches` queues up to 100 requests for one endpoint and answers right away. Each request becomes its own job, costs **half** the realtime price, and is delivered by polling or by webhook. | | Realtime (`POST /v1/search`, …) | Batch (`POST /v1/batches`) | | --- | --- | --- | | Requests per call | 1 | 1 to 100, all for one endpoint | | Response | `200` with the result | `202` with one job per request | | Latency | Seconds | Target: done within 15 minutes | | Price | Normal price | **0.5×** the realtime price | | Delivery | In the response | Poll `GET /v1/batches/{id}` or webhook | Realtime endpoints take one JSON object per call. A JSON array body is rejected with `400 invalid_request`; to run many queries, either send them in parallel within your [rate limit](https://serpkite.com/docs/rate-limits) or queue them here. ## Create a batch Send the endpoint name, the request bodies, and optionally a `webhook_url`: cURL: ```bash curl https://api.serpkite.com/v1/batches \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"endpoint":"search","requests":[{"q":"best espresso machine","country":"us"},{"q":"best espresso machine","country":"de","language":"de"}],"webhook_url":"https://example.com/hooks/serpkite"}' # then poll a job (free) curl https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90 \ -H "Authorization: Bearer $SERPKITE_API_KEY" ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); const { batches } = await sk.batches.create({ endpoint: "search", requests: [ { q: "best espresso machine", country: "us" }, { q: "best espresso machine", country: "de", language: "de" }, ], }); const done = await sk.batches.wait(batches[0].id); // polls until done or failed console.log(done.status, done.result?.results[0].title); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() job = sk.batches.create( endpoint="search", requests=[ {"q": "best espresso machine", "country": "us"}, {"q": "best espresso machine", "country": "de", "language": "de"}, ], ) done = sk.batches.wait(job.batches[0].id) # polls until done or failed print(done.status, done.credits_used) ``` Go: ```go c := serpkite.NewClient() job, err := c.Batches.Create(ctx, serpkite.BatchCreateParams{ Endpoint: serpkite.EndpointSearch, Requests: []any{ serpkite.SearchParams{Q: "best espresso machine", Country: "us"}, serpkite.SearchParams{Q: "best espresso machine", Country: "de", Language: "de"}, }, }) if err != nil { log.Fatal(err) } done, err := c.Batches.Wait(ctx, job.Queued()[0].ID) if err != nil { log.Fatal(err) } fmt.Println(done.Status, done.CreditsUsed) ``` | Field | Type | Description | | --- | --- | --- | | `endpoint` **required** | string | One of `search`, `images`, `videos`, `news`, `maps`, `places`, `reviews`, `shopping`, `scholar`, `patents`, `autocomplete`, `lens`, `webpage`. | | `requests` **required** | array | 1 to 100 request objects. Each takes the same fields as the realtime endpoint, including `num: 100`, `format`, `fields`, `include_content` and `max_age`. | | `webhook_url` | string | HTTPS URL that receives each finished job. Defaults to the account webhook set in the dashboard. See [Webhooks](https://serpkite.com/docs/webhooks). | The response is `202 Accepted` with a `batches` array: one entry per request, in the same order. An entry is either a job or, when that request was invalid or could not be queued, an error object. One invalid request doesn't reject the rest. ```json { "batches": [ { "id": "0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "status": "queued", "endpoint": "/v1/search", "created_at": "2026-09-29T08:00:00Z", "completed_at": null, "credits_used": 0, "poll_url": "https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "webhook_url": "https://example.com/hooks/serpkite", "webhook_status": "pending" }, { "error": { "code": "invalid_request", "message": "unknown parameter \"type\": use the endpoint path", "request_id": "req_01J8ZK4M6Q2V7:1" } } ] } ``` Requests are validated with the same [strict rules](https://serpkite.com/docs/parameters) as realtime calls: an unknown or renamed parameter fails that entry with a message naming the replacement. ### Safe retries with `Idempotency-Key` A timeout on `POST /v1/batches` doesn't tell you whether the jobs were queued. Send an `Idempotency-Key` header (1 to 255 printable ASCII characters, for example a UUID you store with the batch) and retry with the same key and the same body: for 24 hours SerpKite returns the first response, with `Idempotent-Replayed: true`, instead of queueing and billing the jobs again. ```bash curl https://api.serpkite.com/v1/batches \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Idempotency-Key: 5f0c7d52-8a8e-4f7e-9d7b-1b6f0f3c2a91" \ -H "Content-Type: application/json" \ -d '{"endpoint":"search","requests":[{"q":"espresso grinder"}]}' ``` The same key with a different body gets `422 idempotency_key_reused`; a retry that arrives while the first call is still running gets `409` (retry shortly). The SDKs take the key as an option (`idempotencyKey`, `idempotency_key`, `serpkite.WithIdempotencyKey`) and, when it is set, also retry server errors and dropped connections. ## Pricing Batch jobs cost half the realtime price, rounded up to the next 0.001 credit: | Request | Realtime | Batch | | --- | --- | --- | | `search` (1 page) | 1 | 0.5 | | `search` with `num: 100` | 7 | 3.5 | | `search` with `include_content: 3` | 4 | 2 | The maximum cost of each job is reserved from your balance when it is queued, so a queued job can't fail later for lack of credits. The jobs of one call are reserved together: if your balance, spend cap, key limit or free daily limit can't cover all of them, none is queued and each valid entry carries the error (for example `insufficient_credits`). If a job fails, or costs less than reserved (for example fewer pages came back), the difference is refunded. Queuing is checked against your [key limit and spend cap](https://serpkite.com/docs/spend-controls) just like a realtime call. The `POST /v1/batches` call itself counts once against your per-second [rate limit](https://serpkite.com/docs/rate-limits), whatever its size; the jobs are paced by the batch lane. ## Get the results ### Wait with an SDK The SDKs poll for you: `sk.batches.wait(id)` in TypeScript, `sk.batches.wait(id)` in Python and `c.Batches.Wait(ctx, id)` in Go return the job once it is `done` or `failed`. See [SDKs](https://serpkite.com/docs/sdks). ### Poll Poll [`GET /v1/batches/{id}`](https://serpkite.com/docs/endpoints/batches) (or the job's `poll_url`) until `status` is `done` or `failed`. Polling is free. | `status` | Meaning | | --- | --- | | `queued` | Waiting for a worker. | | `running` | Being fetched. | | `done` | Finished; `result` holds the same body the realtime endpoint returns (`request`, `results`, `meta`, …). | | `failed` | Could not be completed; `error` has `code` and `message`, and the credits were refunded. | ```bash curl https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90 \ -H "Authorization: Bearer $SERPKITE_API_KEY" ``` ```json { "id": "0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "status": "done", "endpoint": "/v1/search", "created_at": "2026-09-29T08:00:00Z", "completed_at": "2026-09-29T08:03:12Z", "credits_used": 0.5, "poll_url": "https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "webhook_url": null, "webhook_status": null, "error": null, "result": { "request": { "endpoint": "search", "engine": "google", "q": "best espresso machine", "country": "us" }, "results": [{ "position": 1, "title": "…", "link": "https://www.example.com/…", "domain": "example.com" }], "related_searches": [], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 0.5, "cached": false } } } ``` Poll every few seconds at most; jobs don't finish faster if you poll harder. For large volumes, prefer webhooks. ### Webhooks Set `webhook_url` on the batch (or a default webhook URL in the dashboard under **Settings**) and SerpKite POSTs each finished job to it, always signed with your account's webhook secret. Then you don't need to poll at all. Delivery format, signature verification and retries are covered in [Webhooks](https://serpkite.com/docs/webhooks). ## Limits and retention - 1 to 100 requests per `POST /v1/batches` call, all for the same endpoint. - Up to 10,000 queued jobs per account. Beyond that, new batches get `429 rate_limited` until some finish. - Jobs target completion within 15 minutes. That is a target for the lane, not a per-job guarantee. Workers take jobs round-robin across accounts, so a large backlog from one account doesn't hold up another's jobs. - A job's `result` is kept for 24 hours after it finishes, then deleted. The stored request (which contains your query) is deleted as soon as the job completes. - You can list recent jobs and their status in the dashboard. Related: [Batches endpoint](https://serpkite.com/docs/endpoints/batches), [Webhooks](https://serpkite.com/docs/webhooks), [Rank tracking](https://serpkite.com/docs/guides/rank-tracking), [Credits and billing](https://serpkite.com/docs/credits-and-billing). --- Source: https://serpkite.com/docs/webhooks # Webhooks > Receive finished batch jobs from POST /v1/batches as signed HTTP POSTs. Payload format, headers, HMAC-SHA256 signature verification in Node.js, Python, Go and PHP, and the retry schedule. When a [batch job](https://serpkite.com/docs/batch) finishes, SerpKite can POST the result to a URL you control instead of waiting for you to poll. Deliveries are signed with your account's webhook secret so you can prove they came from SerpKite. ## Set it up 1. ### Create a webhook secret Every delivery is signed. Your account gets a secret automatically with its first webhook; to see it, open **Settings → Webhooks** in the dashboard and click **Rotate secret** (**Generate secret** if none exists yet), which shows the new secret once. The secret starts with `whsec_`; store it as an environment variable such as `SERPKITE_WEBHOOK_SECRET`. 2. ### Choose where deliveries go Either set a default **Webhook URL** in the same settings screen (used by every batch job), or pass `webhook_url` on the [`POST /v1/batches`](https://serpkite.com/docs/batch) call, which takes precedence for every job in that batch: ```bash curl https://api.serpkite.com/v1/batches \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"endpoint":"search","requests":[{"q":"espresso grinder","num":100}],"webhook_url":"https://example.com/hooks/serpkite"}' ``` Webhooks are for batch jobs only. Realtime endpoints such as `/v1/search` return the result in the response and don't accept `webhook_url`. 3. ### Verify and acknowledge Your endpoint checks the signature, stores the result, and answers with any `2xx` status within 10 seconds. Do slow work after responding. ## The delivery SerpKite sends one `POST` per finished job, whether it succeeded or failed. | Header | Value | | --- | --- | | `Content-Type` | `application/json` | | `User-Agent` | `SerpKite-Webhooks/1.0` | | `X-SerpKite-Event` | `batch.completed` | | `X-SerpKite-Delivery` | The job ID. The same on every retry, so use it to deduplicate. | | `X-SerpKite-Timestamp` | Unix time (seconds) when this attempt was signed. | | `X-SerpKite-Signature` | `v1=` followed by the hex HMAC-SHA256 signature. | The body is the job, with the result inline. Fields: `event`, `id`, `status` (`done` or `failed`), `endpoint`, `created_at`, `completed_at`, `credits_used`, and either `result` (the same body the realtime endpoint returns) or `error`. ```json { "event": "batch.completed", "id": "0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "status": "done", "endpoint": "/v1/search", "created_at": "2026-09-29T08:00:00Z", "completed_at": "2026-09-29T08:03:12Z", "credits_used": 3.5, "result": { "request": { "endpoint": "search", "engine": "google", "q": "espresso grinder", "num": 100 }, "results": ["…"], "related_searches": [], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 3.5, "cached": false } } } ``` When `status` is `failed`, there is no `result`; instead `error` holds `code` and `message`, and the job's credits were refunded. The result is also available from [`GET /v1/batches/{id}`](https://serpkite.com/docs/endpoints/batches) for 24 hours, so a lost delivery is never lost data. ## Verifying the signature The signature is an HMAC-SHA256 over the timestamp, a dot, and the **raw request body**, keyed with your webhook secret: ```text X-SerpKite-Signature: v1=hex( HMAC_SHA256( secret, timestamp + "." + raw_body ) ) ``` To verify: 1. Read the raw body bytes before any JSON parsing. Re-serialised JSON will not match. 2. Compute the HMAC with your secret and compare it to the header using a constant-time comparison. 3. Reject timestamps older than a few minutes (5 minutes is a good default) to block replays. 4. Deduplicate on `X-SerpKite-Delivery`, since a retry can arrive after you already processed the job. ### With an SDK The official SDKs verify the signature and the timestamp (5-minute tolerance) for you. Pass the raw body: ```typescript import { verifyWebhook } from "serpkite"; const ok = await verifyWebhook(process.env.SERPKITE_WEBHOOK_SECRET!, rawBody, req.headers); ``` ```python from serpkite import verify_webhook ok = verify_webhook(os.environ["SERPKITE_WEBHOOK_SECRET"], request.get_data(), request.headers) ``` ```go ok := serpkite.VerifyWebhook(os.Getenv("SERPKITE_WEBHOOK_SECRET"), body, r.Header, time.Now()) ``` ### Node.js ```javascript import crypto from "node:crypto"; import express from "express"; const app = express(); const SECRET = process.env.SERPKITE_WEBHOOK_SECRET; app.post("/hooks/serpkite", express.raw({ type: "application/json" }), (req, res) => { const ts = req.get("X-SerpKite-Timestamp") ?? ""; const sig = req.get("X-SerpKite-Signature") ?? ""; const expected = "v1=" + crypto.createHmac("sha256", SECRET).update(`${ts}.`).update(req.body).digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300; const valid = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); if (!fresh || !valid) return res.status(401).end(); const job = JSON.parse(req.body.toString("utf8")); res.status(204).end(); // acknowledge first, then process handleJob(req.get("X-SerpKite-Delivery"), job); }); ``` ### Python ```python import hashlib, hmac, os, time from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ["SERPKITE_WEBHOOK_SECRET"].encode() @app.post("/hooks/serpkite") def serpkite_hook(): ts = request.headers.get("X-SerpKite-Timestamp", "") sig = request.headers.get("X-SerpKite-Signature", "") raw = request.get_data() # raw bytes, not request.json expected = "v1=" + hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest() if not ts.isdigit() or abs(time.time() - int(ts)) > 300: abort(401) if not hmac.compare_digest(sig, expected): abort(401) job = request.get_json() enqueue(request.headers["X-SerpKite-Delivery"], job) # your own queue return "", 204 ``` ### Go ```go package hooks import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "math" "net/http" "os" "strconv" "time" ) var secret = []byte(os.Getenv("SERPKITE_WEBHOOK_SECRET")) func SerpKite(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 32<<20)) if err != nil { http.Error(w, "bad body", http.StatusBadRequest) return } ts := r.Header.Get("X-SerpKite-Timestamp") sec, err := strconv.ParseInt(ts, 10, 64) if err != nil || math.Abs(float64(time.Now().Unix()-sec)) > 300 { http.Error(w, "stale", http.StatusUnauthorized) return } mac := hmac.New(sha256.New, secret) mac.Write([]byte(ts + ".")) mac.Write(body) expected := "v1=" + hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expected), []byte(r.Header.Get("X-SerpKite-Signature"))) { http.Error(w, "bad signature", http.StatusUnauthorized) return } w.WriteHeader(http.StatusNoContent) go process(r.Header.Get("X-SerpKite-Delivery"), body) } ``` ### PHP ```php 300 || !hash_equals($expected, $sig)) { http_response_code(401); exit; } $job = json_decode($raw, true); http_response_code(204); // store $job keyed by $_SERVER['HTTP_X_SERPKITE_DELIVERY'] ``` ## Retries A delivery succeeds when your endpoint answers with any `2xx` status within 10 seconds. Anything else (a timeout, a connection error, a `3xx`, `4xx` or `5xx`) counts as a failure and is retried with exponential backoff: about 1 minute, then 2, 4, 8 and so on, up to roughly an hour apart, for up to 8 attempts in total. The job's `webhook_status` tracks the outcome and is visible on [`GET /v1/batches/{id}`](https://serpkite.com/docs/endpoints/batches) and in the dashboard: | `webhook_status` | Meaning | | --- | --- | | `pending` | Not delivered yet, or waiting for the next retry. | | `delivered` | Your endpoint returned `2xx`. | | `failed` | All attempts failed. Fetch the result by polling within 24 hours. | Each retry is signed again with a fresh timestamp, so always verify against the headers of the request you received. ## Rotating the secret Generate a new secret under **Settings → Webhooks** at any time. The new secret takes effect for the next delivery attempt and the old one stops working, so deploy the new value to your receiver first (accepting either secret for a short overlap), then rotate. ## Troubleshooting - **Signature never matches:** you are hashing parsed-and-reserialised JSON, or a framework decompressed or re-encoded the body. Hash the exact bytes you received. - **Duplicate processing:** retries reuse `X-SerpKite-Delivery`; store processed IDs and skip repeats. - **Deliveries time out:** respond before doing the work. Results for `num: 100` or `include_content` can be large, so allow bodies of several megabytes. - **Local development:** expose your machine with a tunnel (for example `cloudflared tunnel` or `ngrok`) and use that URL as `webhook_url`. Related: [Batch requests](https://serpkite.com/docs/batch), [Batches endpoint](https://serpkite.com/docs/endpoints/batches), [Spend controls](https://serpkite.com/docs/spend-controls). --- Source: https://serpkite.com/docs/include-content # Page content > Fetch the top search results as clean Markdown in the same /v1/search call with include_content. One request instead of a search plus N page fetches, +1 credit per page. Snippets are often not enough to answer a question. With `include_content`, SerpKite runs the search and then fetches the top N organic results for you, converts each page to clean Markdown and puts it into `results[].content`. One request replaces a search plus N separate scrapes, which is the usual first step of a [RAG pipeline](https://serpkite.com/docs/guides/rag-pipeline). cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"how does retrieval augmented generation work","country":"us","include_content":3}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "how does retrieval augmented generation work", country: "us", include_content: 3 }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("how does retrieval augmented generation work", country="us", include_content=3) print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "how does retrieval augmented generation work", Country: "us", IncludeContent: 3}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` ## Parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `include_content` | integer | `0` | Also fetch the top N result pages (0–5) as Markdown. +1 credit per page. | - Only on [/v1/search](https://serpkite.com/docs/endpoints/search). Other endpoints return `400 invalid_request`. - `0` (default) to `5`. The first N results are fetched, in position order. - For a single URL you already have, use [/v1/webpage](https://serpkite.com/docs/endpoints/webpage) instead. ## Response Each fetched result gets a `content` field with the page's main content as Markdown, the same extraction [/v1/webpage](https://serpkite.com/docs/endpoints/webpage) uses. Results beyond N, and results whose page could not be fetched, have no `content`. ```json { "request": { "endpoint": "search", "engine": "google", "q": "how does retrieval augmented generation work", "country": "us", "language": "en", "include_content": 3 }, "results": [ { "position": 1, "title": "What is retrieval-augmented generation?", "link": "https://www.example.com/rag-explained", "domain": "example.com", "snippet": "Retrieval-augmented generation (RAG) combines a retriever with a language model…", "content": "# What is retrieval-augmented generation?\n\nRetrieval-augmented generation (RAG) is a technique that…" }, { "position": 2, "title": "RAG, step by step", "link": "https://docs.example.org/rag", "domain": "docs.example.org", "content": "# RAG, step by step\n\n1. Split your documents into chunks…" } ], "related_searches": [], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 4, "cached": false, "engine": "google" } } ``` `include_content` combines with the other output options: - `format: "compact"` keeps `content` on each row of `results`. - `format: "markdown"` puts each fetched page, indented and wrapped in a `content` tag, under its result. - `fields: "results.title,results.link,results.content"` returns only what a retriever needs. ## Pricing The search costs its normal price, and each fetched page adds 1 credit: | Request | Credits | | --- | --- | | `/v1/search` | 1 | | `/v1/search` + `include_content: 3` | up to 4 | | `/v1/search` + `include_content: 5` | up to 6 | | `/v1/search` + `include_content: 3` via `POST /v1/batches` | up to 2 | The maximum is reserved when the request starts. Pages that can't be fetched (timeouts, blocked or non-HTML pages) are not billed, and the unused part of the reservation is refunded, so `meta.credits_used` and `X-Credits-Used` show what you actually paid. ## Limits and good practice - Only public pages are fetched, the same way a logged-out visitor sees them. Nothing behind a login or paywall is retrieved. - Fetching pages adds latency, since the slowest of the N pages bounds the response. Use a client timeout of at least 60 seconds, or queue the request with [`POST /v1/batches`](https://serpkite.com/docs/batch). - Page content can be long. Check `X-Tokens-Estimate` before putting everything into a prompt, and chunk or truncate on your side. Related: [Webpage endpoint](https://serpkite.com/docs/endpoints/webpage), [RAG pipeline](https://serpkite.com/docs/guides/rag-pipeline), [Output formats](https://serpkite.com/docs/output-formats). --- Source: https://serpkite.com/docs/endpoints/search # Google Search `POST https://api.serpkite.com/v1/search` · Credits: 1 per page (7 for `num=100`) Organic results, knowledge graph, answer box, People Also Ask, related searches, top stories and sitelinks. The main Google web search endpoint. One call returns the whole results page: organic results in `results` with resolved destination URLs, knowledge graph, answer box, People Also Ask, related searches, top stories and the local pack when Google shows them. `related_searches` is always present (an empty array when Google shows none); the other extras appear only when the page has them. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `location` | string | | Canonical location for local results, e.g. "Austin, Texas, United States". Overrides country for geo. | | `uule` | string | | Google-encoded location string. Use instead of location if you already have it. | | `num` | integer | `10` | Results per call. 10 per page; 100 fetches the top 100 as a depth bundle for 7 credits instead of 10. One of: `10`, `20`, `30`, `50`, `100`. | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | | `time` | string | | Restrict to recent results. Shorthand for tbs=qdr:*. One of: `hour`, `day`, `week`, `month`, `year`. | | `tbs` | string | | Raw Google tbs filter, e.g. qdr:w or cdr:1,cd_min:… | | `device` | string | `desktop` | Which SERP layout to fetch. One of: `desktop`, `mobile`. | | `safe` | string | `off` | SafeSearch filtering. One of: `off`, `active`. | | `autocorrect` | boolean | `true` | Let Google correct misspelled queries. Set false to search the exact text. | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `include_content` | integer | `0` | Also fetch the top N result pages (0–5) as Markdown. +1 credit per page. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | | `ads` | boolean | `false` | Include sponsored results in ads. | | `engine` | string or array | `google` | Which search providers may answer. google is Google only (SerpKite still fails over across its own proxy pools); auto falls back to other providers when Google is blocked or times out; consensus (search only) asks several independent indexes in parallel, merges the results by URL, ranks them by agreement and lists each result's sources, at the sum of one page per provider that returned results; a provider name or a list (e.g. google,brave) restricts the request to those. meta.engine names the provider that answered. One of: `google`, `auto`, `consensus`, `brave`, `bing`, `yahoo`, `duckduckgo`, `mojeek`, `wikipedia`. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","country":"us","language":"en"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine 2026", country: "us", language: "en" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine 2026", country="us", language="en") print(res.results[0].title, res.meta.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | Organic results: `position`, `title`, `link` (resolved), `domain`, `displayed_link`, `snippet`, `date`, `sitelinks[]`, `attributes`, `rating`, `rating_count`, `content` (with `include_content`). | | `answer_box` | object | `title`, `answer`, `snippet`, `snippet_highlighted[]`, `link`. | | `knowledge_graph` | object | `title`, `type`, `website`, `image_url`, `description`, `description_source`, `description_link`, `attributes`. | | `people_also_ask[]` | array | `question`, `snippet`, `title`, `link`. | | `related_searches[]` | array | `query`. | | `top_stories[]` | array | News items: `position`, `title`, `link`, `domain`, `source`, `date`, `image_url`. | | `places[]` | array | Local pack, same shape as [/v1/maps](https://serpkite.com/docs/endpoints/maps) results. | | `ads[]` | array | Sponsored results (only with `ads: true`). | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "search", "engine": "google", "q": "best espresso machine 2026", "country": "us", "language": "en", "num": 10, "page": 1, "device": "desktop", "autocorrect": true }, "results": [ { "position": 1, "title": "The Best Espresso Machines of 2026, Tested and Reviewed", "link": "https://www.example.com/best-espresso-machines", "domain": "example.com", "displayed_link": "https://www.example.com › best-espresso-machines", "snippet": "We pulled more than 1,200 shots on 42 machines to find the best espresso makers for every budget, from beginner-friendly to prosumer.", "date": "Sep 12, 2026", "sitelinks": [ { "title": "Best budget pick", "link": "https://www.example.com/best-espresso-machines#budget" }, { "title": "Best dual boiler", "link": "https://www.example.com/best-espresso-machines#dual-boiler" } ] }, { "position": 2, "title": "Espresso Machine Buying Guide (2026)", "link": "https://coffee.example.org/guides/espresso", "domain": "coffee.example.org", "displayed_link": "https://coffee.example.org › guides › espresso", "snippet": "Single boiler, heat exchanger or dual boiler? What the specs mean and which features are worth paying for." }, { "position": 3, "title": "r/espresso: What machine would you buy in 2026?", "link": "https://www.reddit.com/r/espresso/comments/abc123/", "domain": "reddit.com", "displayed_link": "https://www.reddit.com › r › espresso", "snippet": "Discussion thread with 480 comments comparing entry-level and prosumer machines." } ], "people_also_ask": [ { "question": "What is the #1 rated espresso machine?", "snippet": "Reviewers most often rank dual-boiler machines with PID control at the top…", "link": "https://www.example.com/best-espresso-machines" }, { "question": "Is a $500 espresso machine worth it?", "snippet": "For daily drinkers, a mid-range machine usually pays for itself within a year…", "link": "https://coffee.example.org/guides/espresso" } ], "related_searches": [ { "query": "best espresso machine under $500" }, { "query": "best espresso machine for beginners" }, { "query": "dual boiler vs heat exchanger" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 942, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Set `format: "markdown"` or `"compact"` to cut tokens, or project fields with `fields` (e.g. `results.title,results.link,knowledge_graph`). See [Output formats](https://serpkite.com/docs/output-formats). - `num: 100` returns the top 100 in one call for 7 credits. See [Pagination and depth](https://serpkite.com/docs/pagination-and-depth). - `GET /v1/search?q=…` works too, with the same parameters in the query string. ## Related - [Output formats](https://serpkite.com/docs/output-formats) - [Page content](https://serpkite.com/docs/include-content) - [Official SDKs](https://serpkite.com/docs/sdks) --- Source: https://serpkite.com/docs/endpoints/news # Google News `POST https://api.serpkite.com/v1/news` · Credits: 1 per page News articles with source, publish date, snippet and thumbnail. Google News results for a query: headline, source, publish date, snippet and thumbnail. Combine with `time` to watch a topic over the last hour, day or week. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `location` | string | | Canonical location for local results, e.g. "Austin, Texas, United States". Overrides country for geo. | | `uule` | string | | Google-encoded location string. Use instead of location if you already have it. | | `num` | integer | `10` | Results per call. 10 per page; 100 fetches the top 100 as a depth bundle for 7 credits instead of 10. One of: `10`, `20`, `30`, `50`, `100`. | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | | `time` | string | | Restrict to recent results. Shorthand for tbs=qdr:*. One of: `hour`, `day`, `week`, `month`, `year`. | | `tbs` | string | | Raw Google tbs filter, e.g. qdr:w or cdr:1,cd_min:… | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | | `engine` | string or array | `google` | Which search providers may answer. google is Google only (SerpKite still fails over across its own proxy pools); auto falls back to other providers when Google is blocked or times out; consensus (search only) asks several independent indexes in parallel, merges the results by URL, ranks them by agreement and lists each result's sources, at the sum of one page per provider that returned results; a provider name or a list (e.g. google,brave) restricts the request to those. meta.engine names the provider that answered. One of: `google`, `auto`, `consensus`, `brave`, `bing`, `yahoo`, `duckduckgo`, `mojeek`, `wikipedia`. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/news \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"AI agents funding","country":"us","time":"week"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.news({ q: "AI agents funding", country: "us", time: "week" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.news("AI agents funding", country="us", time="week") print(res.results[0].title, res.meta.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | `position`, `title`, `link`, `domain`, `snippet`, `date`, `source`, `image_url`. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "news", "engine": "google", "q": "AI agents funding", "country": "us", "language": "en", "time": "week" }, "results": [ { "position": 1, "title": "Agent startups raised a record amount this quarter", "link": "https://news.example.com/2026/09/agent-startups-funding", "domain": "news.example.com", "snippet": "Investors poured money into companies building autonomous software agents…", "date": "3 hours ago", "source": "Example News", "image_url": "https://news.example.com/img/agents.jpg" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Dates are shown as Google displays them (e.g. "3 hours ago"). ## Related - [Localization](https://serpkite.com/docs/localization) - [Batch requests](https://serpkite.com/docs/batch) --- Source: https://serpkite.com/docs/endpoints/images # Google Images `POST https://api.serpkite.com/v1/images` · Credits: 1 per page Image results with source page, dimensions, thumbnail and original URL. Google Images results with the original image URL, dimensions, thumbnail and the page it was found on. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `num` | integer | `10` | Results per call. 10 per page; 100 fetches the top 100 as a depth bundle for 7 credits instead of 10. One of: `10`, `20`, `30`, `50`, `100`. | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | | `time` | string | | Restrict to recent results. Shorthand for tbs=qdr:*. One of: `hour`, `day`, `week`, `month`, `year`. | | `tbs` | string | | Raw Google tbs filter, e.g. qdr:w or cdr:1,cd_min:… | | `safe` | string | `off` | SafeSearch filtering. One of: `off`, `active`. | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | | `engine` | string or array | `google` | Which search providers may answer. google is Google only (SerpKite still fails over across its own proxy pools); auto falls back to other providers when Google is blocked or times out; consensus (search only) asks several independent indexes in parallel, merges the results by URL, ranks them by agreement and lists each result's sources, at the sum of one page per provider that returned results; a provider name or a list (e.g. google,brave) restricts the request to those. meta.engine names the provider that answered. One of: `google`, `auto`, `consensus`, `brave`, `bing`, `yahoo`, `duckduckgo`, `mojeek`, `wikipedia`. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/images \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"mid-century modern living room","country":"us"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.images({ q: "mid-century modern living room", country: "us" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.images("mid-century modern living room", country="us") print(res.results[0].title, res.meta.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | `position`, `title`, `image_url`, `image_width`, `image_height`, `thumbnail_url`, `source`, `domain`, `link`. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "images", "engine": "google", "q": "mid-century modern living room", "country": "us", "language": "en" }, "results": [ { "position": 1, "title": "Mid-century modern living room ideas", "image_url": "https://images.example.com/living-room.jpg", "image_width": 1600, "image_height": 1067, "thumbnail_url": "https://images.example.com/living-room-thumb.jpg", "source": "Example Interiors", "domain": "interiors.example.com", "link": "https://interiors.example.com/mid-century-living-room" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - `safe: "active"` enables SafeSearch. ## Related - [Lens (search by image)](https://serpkite.com/docs/endpoints/lens) --- Source: https://serpkite.com/docs/endpoints/videos # Google Videos `POST https://api.serpkite.com/v1/videos` · Credits: 1 per page Video results with channel, duration, date and thumbnail. Google Videos results with channel, duration, publish date and thumbnail. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `num` | integer | `10` | Results per call. 10 per page; 100 fetches the top 100 as a depth bundle for 7 credits instead of 10. One of: `10`, `20`, `30`, `50`, `100`. | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | | `time` | string | | Restrict to recent results. Shorthand for tbs=qdr:*. One of: `hour`, `day`, `week`, `month`, `year`. | | `tbs` | string | | Raw Google tbs filter, e.g. qdr:w or cdr:1,cd_min:… | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | | `engine` | string or array | `google` | Which search providers may answer. google is Google only (SerpKite still fails over across its own proxy pools); auto falls back to other providers when Google is blocked or times out; consensus (search only) asks several independent indexes in parallel, merges the results by URL, ranks them by agreement and lists each result's sources, at the sum of one page per provider that returned results; a provider name or a list (e.g. google,brave) restricts the request to those. meta.engine names the provider that answered. One of: `google`, `auto`, `consensus`, `brave`, `bing`, `yahoo`, `duckduckgo`, `mojeek`, `wikipedia`. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/videos \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"how to pull an espresso shot","country":"us"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.videos({ q: "how to pull an espresso shot", country: "us" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.videos("how to pull an espresso shot", country="us") print(res.results[0].title, res.meta.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | `position`, `title`, `link`, `domain`, `snippet`, `image_url`, `duration`, `source`, `channel`, `date`. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "videos", "engine": "google", "q": "how to pull an espresso shot", "country": "us", "language": "en" }, "results": [ { "position": 1, "title": "How to pull the perfect espresso shot", "link": "https://video.example.com/watch?v=abc123", "domain": "video.example.com", "snippet": "Dose, grind, tamp and time: a step-by-step guide…", "image_url": "https://video.example.com/thumb/abc123.jpg", "duration": "8:42", "source": "Example Video", "channel": "Home Barista Lab", "date": "Mar 3, 2026" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Related - [Search](https://serpkite.com/docs/endpoints/search) --- Source: https://serpkite.com/docs/endpoints/maps # Google Maps `POST https://api.serpkite.com/v1/maps` · Credits: 1 per page Local businesses with place_id, rating, review count, coordinates, hours, phone and website. Google Maps search: businesses and points of interest with coordinates, rating, review count, hours, phone, website and the IDs you need for [/v1/reviews](https://serpkite.com/docs/endpoints/reviews). Pass `location` for a city-level search, or `ll` for an exact viewport. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `location` | string | | Canonical location for local results, e.g. "Austin, Texas, United States". Overrides country for geo. | | `uule` | string | | Google-encoded location string. Use instead of location if you already have it. | | `ll` | string | | Maps only: viewport as "@lat,lng,zoom", e.g. "@52.52,13.40,14z". | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/maps \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"coffee roasters","location":"Berlin, Germany"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.maps({ q: "coffee roasters", location: "Berlin, Germany" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.maps("coffee roasters", location="Berlin, Germany") print(res.results[0].title, res.meta.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | `position`, `title`, `address`, `latitude`, `longitude`, `rating`, `rating_count`, `price_level`, `type`, `types[]`, `website`, `phone_number`, `opening_hours`, `thumbnail_url`, `place_id`, `cid`, `fid`. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "maps", "engine": "google", "q": "coffee roasters", "location": "Berlin, Germany", "language": "en" }, "results": [ { "position": 1, "title": "Example Coffee Roasters", "address": "Beispielstraße 1, 10115 Berlin", "latitude": 52.5321, "longitude": 13.3849, "rating": 4.7, "rating_count": 812, "price_level": "€€", "type": "Coffee roasters", "types": [ "Coffee roasters", "Cafe" ], "website": "https://coffee.example.de", "phone_number": "+49 30 0000000", "opening_hours": { "Monday": "8 AM–6 PM", "Sunday": "10 AM–5 PM" }, "place_id": "ChIJexample0000000000000", "cid": "1234567890123456789" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Use `place_id`, `cid` or `fid` from a result to fetch its reviews. ## Related - [Places](https://serpkite.com/docs/endpoints/places) - [Reviews](https://serpkite.com/docs/endpoints/reviews) - [Localization](https://serpkite.com/docs/localization) --- Source: https://serpkite.com/docs/endpoints/places # Google Places `POST https://api.serpkite.com/v1/places` · Credits: 1 per page Local pack results from the Places tab: name, address, rating, category, CID. Local results as shown in Google's Places tab: name, address, rating, category and IDs. Lighter than [/v1/maps](https://serpkite.com/docs/endpoints/maps) when you don't need coordinates for every result. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `location` | string | | Canonical location for local results, e.g. "Austin, Texas, United States". Overrides country for geo. | | `uule` | string | | Google-encoded location string. Use instead of location if you already have it. | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/places \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"dentist","location":"Austin, Texas, United States"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.places({ q: "dentist", location: "Austin, Texas, United States" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.places("dentist", location="Austin, Texas, United States") print(res.results[0].title, res.meta.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | Same shape as [/v1/maps](https://serpkite.com/docs/endpoints/maps): `title`, `address`, `rating`, `rating_count`, `type`, `phone_number`, `website`, `place_id`, `cid`… | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "places", "engine": "google", "q": "dentist", "location": "Austin, Texas, United States", "language": "en" }, "results": [ { "position": 1, "title": "Example Family Dental", "address": "100 Example St, Austin, TX 78701", "rating": 4.9, "rating_count": 356, "type": "Dentist", "phone_number": "(512) 555-0100", "website": "https://dental.example.com", "cid": "9876543210987654321" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Related - [Maps](https://serpkite.com/docs/endpoints/maps) - [Localization](https://serpkite.com/docs/localization) --- Source: https://serpkite.com/docs/endpoints/reviews # Google Reviews `POST https://api.serpkite.com/v1/reviews` · Credits: 1 per 10 reviews Reviews for a place (by place_id, cid or fid), 10 per credit, sortable by newest or rating. Page with next_page_token. Reviews for one place, identified by `place_id`, `cid` or `fid` (all three come back from [/v1/maps](https://serpkite.com/docs/endpoints/maps) and [/v1/places](https://serpkite.com/docs/endpoints/places)). Each call returns up to `num` reviews in `results` and a `next_page_token` for the next page. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `place_id` | string | | Google place ID. One of `place_id`, `cid` or `fid` is required. | | `cid` | string | | Reviews: Google customer ID of the place. | | `fid` | string | | Reviews: Google feature ID of the place. | | `sort` | string | `most_relevant` | Reviews: sort order. One of: `most_relevant`, `newest`, `highest_rating`, `lowest_rating`. | | `page_token` | string | | Reviews: next_page_token from the previous page, unchanged (bound to the place and sort). Paging reaches the first 100 reviews per sort order. | | `num` | integer | `10` | Reviews per call, up to 50. Billed 1 credit per 10. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/reviews \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"place_id":"ChIJLU7jZClu5kcR4PcOOO6p3I0","sort":"newest"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.reviews({ place_id: "ChIJLU7jZClu5kcR4PcOOO6p3I0", sort: "newest" }); console.log(res.results.length, res.next_page_token); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.reviews(place_id="ChIJLU7jZClu5kcR4PcOOO6p3I0", sort="newest") print(len(res.results), res.next_page_token) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | `rating`, `date`, `iso_date`, `snippet`, `likes`, `user` (`name`, `thumbnail`, `reviews`), `response` (`snippet`, `date`) for owner replies. | | `next_page_token` | string | Pass back as `page_token` (with the same place and `sort`) to get the next page. Absent on the last page and once the first 100 reviews are reached. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "reviews", "engine": "google", "place_id": "ChIJLU7jZClu5kcR4PcOOO6p3I0", "sort": "newest", "num": 10, "language": "en" }, "results": [ { "rating": 5, "date": "2 weeks ago", "iso_date": "2026-09-14T10:21:00Z", "snippet": "Great view from the top, book tickets online to skip the queue.", "likes": 3, "user": { "name": "Alex", "reviews": 41 } } ], "next_page_token": "CAESBkVnSUlDZw", "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Use `sort: "newest"` with `max_age` to monitor new reviews cheaply. ## Related - [Maps](https://serpkite.com/docs/endpoints/maps) - [Places](https://serpkite.com/docs/endpoints/places) --- Source: https://serpkite.com/docs/endpoints/shopping # Google Shopping `POST https://api.serpkite.com/v1/shopping` · Credits: 1 per page Products with price, merchant, rating, review count and delivery info. Google Shopping products with price (as shown and as a number), merchant, rating, delivery info and product ID. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `location` | string | | Canonical location for local results, e.g. "Austin, Texas, United States". Overrides country for geo. | | `uule` | string | | Google-encoded location string. Use instead of location if you already have it. | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/shopping \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"noise cancelling headphones","country":"us"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.shopping({ q: "noise cancelling headphones", country: "us" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.shopping("noise cancelling headphones", country="us") print(res.results[0].title, res.meta.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | `position`, `title`, `source`, `link`, `price`, `price_value`, `currency`, `delivery`, `image_url`, `rating`, `rating_count`, `offers`, `product_id`. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "shopping", "engine": "google", "q": "noise cancelling headphones", "country": "us", "language": "en" }, "results": [ { "position": 1, "title": "Example ANC Wireless Headphones", "source": "Example Store", "link": "https://store.example.com/p/anc-headphones", "price": "$249.00", "price_value": 249, "currency": "USD", "delivery": "Free delivery", "rating": 4.6, "rating_count": 2104, "offers": "12 stores", "product_id": "1234567890" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Prices depend on `country` and `location`. ## Related - [Localization](https://serpkite.com/docs/localization) --- Source: https://serpkite.com/docs/endpoints/scholar # Google Scholar `POST https://api.serpkite.com/v1/scholar` · Credits: 1 per page Papers with authors, publication, year, citation count and PDF links. Google Scholar papers with authors and publication, year, citation count and a direct PDF link when one exists. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `num` | integer | `10` | Results per call. 10 per page; 100 fetches the top 100 as a depth bundle for 7 credits instead of 10. One of: `10`, `20`, `30`, `50`, `100`. | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/scholar \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"retrieval augmented generation"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.scholar({ q: "retrieval augmented generation" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.scholar("retrieval augmented generation") print(res.results[0].title, res.meta.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | `position`, `title`, `link`, `domain`, `publication_info`, `snippet`, `year`, `cited_by`, `pdf_url`, `id`. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "scholar", "engine": "google", "q": "retrieval augmented generation", "language": "en" }, "results": [ { "position": 1, "title": "Retrieval-augmented generation for knowledge-intensive NLP tasks", "link": "https://papers.example.org/rag", "domain": "papers.example.org", "publication_info": "P Lewis, E Perez, A Piktus… - Advances in Neural Information Processing Systems, 2020", "snippet": "Large pre-trained language models have been shown to store factual knowledge…", "year": 2020, "cited_by": 9000, "pdf_url": "https://papers.example.org/rag.pdf", "id": "abcDEF123" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Related - [RAG pipeline](https://serpkite.com/docs/guides/rag-pipeline) --- Source: https://serpkite.com/docs/endpoints/patents # Google Patents `POST https://api.serpkite.com/v1/patents` · Credits: 1 per page Patents with number, assignee, inventor, priority and publication dates, PDF. Google Patents results with publication number, assignee, inventor, priority, filing, grant and publication dates, and the PDF. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `num` | integer | `10` | Results per call. 10 per page; 100 fetches the top 100 as a depth bundle for 7 credits instead of 10. One of: `10`, `20`, `30`, `50`, `100`. | | `page` | integer | `1` | Results page, 1–10. Each page is billed separately. | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/patents \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"solid state battery electrolyte"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.patents({ q: "solid state battery electrolyte" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.patents("solid state battery electrolyte") print(res.results[0].title, res.meta.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | `position`, `title`, `snippet`, `link`, `publication_number`, `priority_date`, `filing_date`, `grant_date`, `publication_date`, `inventor`, `assignee`, `language`, `pdf_url`, `thumbnail_url`. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "patents", "engine": "google", "q": "solid state battery electrolyte" }, "results": [ { "position": 1, "title": "Solid electrolyte composition and all-solid-state battery", "snippet": "A sulfide solid electrolyte with improved ionic conductivity…", "link": "https://patents.example.com/patent/US0000000A1", "publication_number": "US0000000A1", "priority_date": "2024-03-01", "filing_date": "2025-02-27", "publication_date": "2026-09-03", "inventor": "Jane Example", "assignee": "Example Energy Co", "language": "en", "pdf_url": "https://patents.example.com/pdf/US0000000A1.pdf" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Related - [Scholar](https://serpkite.com/docs/endpoints/scholar) --- Source: https://serpkite.com/docs/endpoints/autocomplete # Google Autocomplete `POST https://api.serpkite.com/v1/autocomplete` · Credits: 0.5 Query suggestions as the user types. Useful for keyword research and query expansion. Google's query suggestions for a prefix. Half a credit per call, which makes it cheap for keyword research and query expansion before a search. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. Up to 2,048 characters. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `format` | string | `json` | Response format. markdown is LLM-ready prose; compact is JSON with only the fields agents need. One of: `json`, `compact`, `markdown`. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/autocomplete \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"how to learn","country":"us"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.autocomplete({ q: "how to learn", country: "us" }); console.log(res.results.map((s) => s.value)); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.autocomplete("how to learn", country="us") print([s.value for s in res.results]) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | `value`: the suggested query. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "autocomplete", "engine": "google", "q": "how to learn", "country": "us", "language": "en" }, "results": [ { "value": "how to learn python" }, { "value": "how to learn spanish" }, { "value": "how to learn guitar" }, { "value": "how to learn to code" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 0.5, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - `format: "markdown"` is not available for this endpoint; the JSON is already small. ## Related - [Tool calling for agents](https://serpkite.com/docs/guides/agents-tool-calling) --- Source: https://serpkite.com/docs/endpoints/lens # Google Lens `POST https://api.serpkite.com/v1/lens` · Credits: 2 Visual matches and related products for an image URL. Google Lens for an image URL: visual matches with title, source page and thumbnails in `results`. The image must be publicly reachable. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `url` **required** | string | | Public URL of the image. Required. | | `country` | string | `us` | Country to search from, as a two-letter ISO code (us, gb, de, in…). | | `language` | string | `en` | Interface language, as a language code (en, de, fr, pt-BR…). | | `fields` | string | | Comma-separated projection, e.g. results.title,results.link,knowledge_graph. Cuts tokens. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/lens \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.lens({ url: "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.lens("https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg") print(res.results[0].title, res.meta.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `results[]` | array | Visual matches: `position`, `title`, `source`, `link`, `domain`, `image_url`, `thumbnail_url`. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "lens", "engine": "google", "url": "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg", "country": "us", "language": "en" }, "results": [ { "position": 1, "title": "Example image on a product page", "source": "Example Shop", "link": "https://shop.example.com/item/42", "domain": "shop.example.com", "image_url": "https://shop.example.com/img/42.jpg", "thumbnail_url": "https://shop.example.com/img/42-thumb.jpg" } ], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 2, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Returns JSON only (no Markdown format). ## Related - [Images](https://serpkite.com/docs/endpoints/images) --- Source: https://serpkite.com/docs/endpoints/webpage # Webpage to Markdown `POST https://api.serpkite.com/v1/webpage` · Credits: 1 per page Fetch any public URL and get clean Markdown plus title, description and metadata. Fetch any public URL and get clean Markdown and plain text plus metadata (title, description, language, canonical, author, published time). Built for feeding pages to an LLM. This is the one endpoint without a `results` list. To fetch the top search results in the same call as the search, use [`include_content`](https://serpkite.com/docs/include-content) on /v1/search instead. ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `url` **required** | string | | Public http(s) URL to fetch. Required. | | `format` | string | `json` | `markdown` returns the page Markdown as `text/markdown`. One of: `json`, `markdown`. | | `include_html` | boolean | `false` | Webpage: also return the raw HTML. | | `max_age` | integer | | Accept a cached result up to this many seconds old. Cache hits cost 50% of the credits. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/webpage \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://en.wikipedia.org/wiki/Search_engine_results_page","format":"markdown"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.webpage({ url: "https://en.wikipedia.org/wiki/Search_engine_results_page", format: "markdown" }); console.log(res); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.webpage("https://en.wikipedia.org/wiki/Search_engine_results_page", format="markdown") print(res) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `url` | string | Final URL after redirects. | | `status_code` | integer | HTTP status of the fetched page. | | `markdown` | string | Main content as Markdown. | | `text` | string | Main content as plain text. | | `html` | string | Raw HTML, only with `include_html: true`. | | `metadata` | object | `title`, `description`, `language`, `canonical`, `site_name`, `image`, `published_time`, `author`. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "webpage", "engine": "google", "url": "https://en.wikipedia.org/wiki/Search_engine_results_page", "format": "json" }, "url": "https://en.wikipedia.org/wiki/Search_engine_results_page", "status_code": 200, "markdown": "# Search engine results page\n\nA **search engine results page** (**SERP**) is a webpage that is displayed by a search engine in response to a query by a user…", "text": "Search engine results page. A search engine results page (SERP) is a webpage that is displayed by a search engine in response to a query by a user…", "metadata": { "title": "Search engine results page - Wikipedia", "language": "en", "canonical": "https://en.wikipedia.org/wiki/Search_engine_results_page", "site_name": "Wikipedia" }, "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 1, "cached": false, "engine": "google", "latency_ms": 1034 } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Only public pages are fetched: no logins, no paywalled content. - Pages that can't be fetched return `503 upstream_error` and are not billed. ## Related - [Page content with search](https://serpkite.com/docs/include-content) - [RAG pipeline](https://serpkite.com/docs/guides/rag-pipeline) --- Source: https://serpkite.com/docs/endpoints/customsearch # Custom Search (CSE-compatible) `GET https://api.serpkite.com/customsearch/v1` · Credits: 1 per page For apps moving off the Google Custom Search JSON API: same query params (key, cx, q, start, num…), same items[] and searchInformation shape. For apps moving off the Google Custom Search JSON API, which Google shuts down on January 1, 2027. Same query parameters, same `items[]` and `searchInformation` shape. Change the host and the key, keep your parser. `cx` is accepted and ignored: results come from the whole web, not a programmable search engine. Use `siteSearch` or a `site:` operator to restrict to a domain. This endpoint keeps Google's own parameter names (`gl`, `hl`, `lr`…) and is not under `/v1`. ## Query parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The search query. Required. | | `key` | string | | Your SerpKite API key (or send `Authorization: Bearer`). | | `cx` | string | | Accepted and ignored. | | `start` | integer | `1` | Index of the first result, 1–91 (1, 11, 21…). | | `num` | integer | `10` | Results per call, 1–10. | | `gl` | string | | Country code (Google CSE name). | | `hl` | string | | Interface language (Google CSE name). | | `lr` | string | | Language restrict, e.g. `lang_de`. | | `safe` | string | | SafeSearch. One of: `active`, `off`. | | `dateRestrict` | string | | Recency, e.g. `d7`, `w2`, `m6`, `y1`. | | `siteSearch` | string | | Restrict to a site. | | `searchType` | string | | `image` for image results. One of: `image`. | ## Example request cURL: ```bash curl "https://api.serpkite.com/customsearch/v1?q=site%3Adocs.python.org+asyncio&cx=any" \ -H "Authorization: Bearer $SERPKITE_API_KEY" ``` Python: ```python import os import requests res = requests.get( "https://api.serpkite.com/customsearch/v1", params={"q": "site:docs.python.org asyncio", "cx": "any"}, headers={"Authorization": f"Bearer {os.environ['SERPKITE_API_KEY']}"}, timeout=30, ) res.raise_for_status() print(res.json()) ``` Node.js: ```javascript const res = await fetch("https://api.serpkite.com/customsearch/v1?q=site%3Adocs.python.org+asyncio&cx=any", { headers: { Authorization: `Bearer ${process.env.SERPKITE_API_KEY}` }, }); if (!res.ok) throw new Error((await res.json()).error.message); console.log(await res.json()); ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `kind` | string | `customsearch#search`. | | `searchInformation` | object | `searchTime`, `formattedSearchTime`, `totalResults`, `formattedTotalResults`. | | `items[]` | array | `kind`, `title`, `htmlTitle`, `link`, `displayLink`, `snippet`, `htmlSnippet`, `formattedUrl`, `htmlFormattedUrl`, `pagemap`, `image`. | | `queries` | object | `request[]` and `nextPage[]` as in the CSE API. | ## Example response ```json { "kind": "customsearch#search", "searchInformation": { "searchTime": 0.94, "formattedSearchTime": "0.94", "totalResults": "1240000", "formattedTotalResults": "1,240,000" }, "items": [ { "kind": "customsearch#result", "title": "asyncio — Asynchronous I/O", "htmlTitle": "asyncio — Asynchronous I/O", "link": "https://docs.python.org/3/library/asyncio.html", "displayLink": "docs.python.org", "snippet": "asyncio is a library to write concurrent code using the async/await syntax." } ], "queries": { "request": [ { "searchTerms": "site:docs.python.org asyncio", "count": 10, "startIndex": 1 } ] } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Errors use SerpKite's error shape (`{"error":{"code","message","request_id"}}`), not Google's. ## Related - [Migrate from Google CSE](https://serpkite.com/docs/guides/migrate-from-google-cse) - [CSE shutdown and alternatives](https://serpkite.com/migrate/google-custom-search-api) --- Source: https://serpkite.com/docs/endpoints/rank # Rank `POST https://api.serpkite.com/v1/rank` · Credits: 7 for the top 100 (priced like /v1/search depth) Where does a domain rank for a keyword? Returns the best organic position in the top N and every matching result. A convenience endpoint for rank trackers: it fetches the top `num` organic results (default 100) and returns only what matched your `domain`, so you don't have to parse 100 results yourself. Subdomains match (`blog.example.com` counts for `example.com`). For large keyword sets, queue them through [`POST /v1/batches`](https://serpkite.com/docs/endpoints/batches) with `endpoint: "search"` and `num: 100` at half price. See [Rank tracking](https://serpkite.com/docs/guides/rank-tracking). ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` **required** | string | | The keyword. | | `domain` **required** | string | | Domain to find, e.g. "example.com". Subdomains match. | | `num` | integer | `100` | How deep to check. One of: `10`, `20`, `30`, `50`, `100`. | | `country` | string | `us` | Country code. | | `language` | string | `en` | Language code. | | `location` | string | | Canonical location for local rankings. | | `device` | string | `desktop` | Device to search as. One of: `desktop`, `mobile`. | | `max_age` | integer | | Accept a cached result up to this many seconds old (half price). | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/rank \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"best espresso machine 2026","domain":"example.com","country":"us"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.rank({ q: "best espresso machine 2026", domain: "example.com", country: "us" }); console.log(res.position, res.checked); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.rank("best espresso machine 2026", "example.com", country="us") print(res.position, res.checked) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `request` | object | The normalised request, with defaults filled in: `endpoint`, `engine`, `q`, `country`, `language`, `location`, `num`, `page`, `device`, `autocorrect`… | | `domain` | string | The normalised domain that was searched for. | | `position` | integer \| null | Best organic position, or null if not in the checked results. | | `matches[]` | array | Every matching result: `position`, `title`, `link`. | | `checked` | integer | How many organic results were inspected. | | `meta` | object | `request_id`, `credits_used`, `cached`, `cached_at`, `engine` (the provider that answered), `route` (provider attempts, see [Search providers](https://serpkite.com/docs/providers)), `latency_ms`, `parse_quality` (`ok`, `partial`, `empty`), `resolved_urls`. | ## Example response ```json { "request": { "endpoint": "rank", "engine": "google", "q": "best espresso machine 2026", "country": "us", "language": "en", "num": 100 }, "domain": "example.com", "position": 1, "matches": [ { "position": 1, "title": "The Best Espresso Machines of 2026, Tested and Reviewed", "link": "https://www.example.com/best-espresso-machines" }, { "position": 14, "title": "Espresso grinder buying guide", "link": "https://www.example.com/grinders" } ], "checked": 100, "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 7, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Failed and empty searches are refunded, like every other endpoint. ## Related - [Rank Tracker API](https://serpkite.com/apis/google-rank-tracker-api) - [Rank tracking guide](https://serpkite.com/docs/guides/rank-tracking) - [Free rank checker](https://serpkite.com/tools/rank-checker) --- Source: https://serpkite.com/docs/endpoints/batches # Batches `POST https://api.serpkite.com/v1/batches` · Credits: Half the realtime price; polling is free Queue up to 100 requests for one endpoint at half price, then poll `GET /v1/batches/{id}` or get a signed webhook. Send one `endpoint` and 1–100 `requests` (the same bodies the realtime endpoint takes). The answer is `202 Accepted` with `batches`: one entry per request, in order, each either a batch job or an error object (for example a request that failed validation). Each job is billed at 0.5× the realtime price, and failed jobs are refunded. Poll `GET /v1/batches/{id}` (free) with a job `id`, or follow its `poll_url`, until `status` is `done` or `failed`. Or pass `webhook_url` (or set an account webhook) and we POST each finished job, signed with `X-SerpKite-Signature`. See [Batch requests](https://serpkite.com/docs/batch) and [Webhooks](https://serpkite.com/docs/webhooks). ## Request body | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `endpoint` **required** | string | | Endpoint every request runs against. One of: `search`, `images`, `videos`, `news`, `maps`, `places`, `reviews`, `shopping`, `scholar`, `patents`, `autocomplete`, `lens`, `webpage`. | | `requests` **required** | array | | 1–100 request objects with the same fields as the realtime endpoint (e.g. `{"q":"…","country":"de"}`). | | `webhook_url` | string | | Where each finished job is POSTed (event `batch.completed`). Defaults to the account webhook set in the dashboard. | ## Example request cURL: ```bash curl https://api.serpkite.com/v1/batches \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"endpoint":"search","requests":[{"q":"best espresso machine","country":"us"},{"q":"best espresso machine","country":"de","language":"de"}],"webhook_url":"https://example.com/hooks/serpkite"}' # then poll a job (free) curl https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90 \ -H "Authorization: Bearer $SERPKITE_API_KEY" ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); const { batches } = await sk.batches.create({ endpoint: "search", requests: [ { q: "best espresso machine", country: "us" }, { q: "best espresso machine", country: "de", language: "de" }, ], }); const done = await sk.batches.wait(batches[0].id); // polls until done or failed console.log(done.status, done.result?.results[0].title); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() job = sk.batches.create( endpoint="search", requests=[ {"q": "best espresso machine", "country": "us"}, {"q": "best espresso machine", "country": "de", "language": "de"}, ], ) done = sk.batches.wait(job.batches[0].id) # polls until done or failed print(done.status, done.credits_used) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `batches[]` | array | `202` response: one entry per request, in order. Each is a job (fields below) or `{"error":{"code","message"}}`. | | `id` | string | Job ID (UUID). | | `status` | string | `queued`, `running`, `done` or `failed`. | | `endpoint` | string | The endpoint the job runs, e.g. `/v1/search`. | | `created_at` | string | ISO 8601 timestamp. | | `completed_at` | string \| null | When the job finished. | | `credits_used` | number | Credits charged (0.5× the realtime price; 0 if failed). | | `poll_url` | string | `https://api.serpkite.com/v1/batches/`. | | `webhook_url` | string \| null | Where the result is POSTed. | | `webhook_status` | string \| null | `pending`, `delivered` or `failed`. | | `error` | object \| null | `code` and `message` when `status` is `failed`. | | `result` | object \| null | The same body the realtime endpoint returns (`request`, `results`, …, `meta`). Present when done; kept for 24 hours. | ## Example response ```json { "id": "0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "status": "done", "endpoint": "/v1/search", "created_at": "2026-09-29T08:00:00Z", "completed_at": "2026-09-29T08:03:12Z", "credits_used": 0.5, "poll_url": "https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "webhook_url": null, "webhook_status": null, "error": null, "result": { "request": { "endpoint": "search", "engine": "google", "q": "best espresso machine", "country": "us", "language": "en" }, "results": [ "…" ], "related_searches": [], "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 0.5, "cached": false, "engine": "google", "latency_ms": 1034, "parse_quality": "ok", "resolved_urls": true } } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 404 | `not_found` | The batch job does not exist, belongs to another account, or its result has expired. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - `GET /v1/batches/{id}` is free. Poll every few seconds at most; jobs target completion within 15 minutes. - Realtime endpoints reject JSON arrays; send many queries through this endpoint instead. - The official SDKs wrap both calls: `sk.batches.create(…)` and `sk.batches.wait(id)`. ## Related - [Batch requests](https://serpkite.com/docs/batch) - [Webhooks](https://serpkite.com/docs/webhooks) - [Official SDKs](https://serpkite.com/docs/sdks) --- Source: https://serpkite.com/docs/endpoints/account # Account `GET https://api.serpkite.com/v1/account` · Credits: Free Balance, rate limit, plan and this month's usage for the account behind the calling key. A free call to check the balance before a large job, or to show usage in your own UI. It also returns the calling key's monthly limit and usage when the key has one. ## Example request cURL: ```bash curl "https://api.serpkite.com/v1/account" \ -H "Authorization: Bearer $SERPKITE_API_KEY" ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); const account = await sk.account(); console.log(account.balance, account.month.credits); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() account = sk.account() print(account.balance, account.month.credits) ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `balance` | number | Remaining credits (decimal). | | `rate_limit_rps` | integer | Requests per second allowed per key. | | `plan` | string | `free` or `paid`. | | `key` | object | `id`, `name`, `credit_limit` (number or null), `credits_used_month`. | | `monthly_spend_cap` | number \| null | Account-wide monthly cap, if set. | | `month` | object | `credits` and `requests` in the current UTC calendar month. | ## Example response ```json { "balance": 61499.5, "rate_limit_rps": 50, "plan": "paid", "key": { "id": "0192f7a4-0000-7000-8000-000000000001", "name": "production", "credit_limit": 20000, "credits_used_month": 3120.5 }, "monthly_spend_cap": 50000, "month": { "credits": 4210.5, "requests": 4388 } } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Not billed. Works with any active key of the account. ## Related - [Credits and billing](https://serpkite.com/docs/credits-and-billing) - [API keys](https://serpkite.com/docs/api-keys) - [Spend controls](https://serpkite.com/docs/spend-controls) --- Source: https://serpkite.com/docs/endpoints/status # Status `GET https://api.serpkite.com/v1/status` · Credits: Free, no key Public live status: request volume, success rate and p50/p95 latency per endpoint over the last hour. No API key needed. This powers our [status page](https://serpkite.com/status) and is safe to poll from your own monitoring (once a minute is plenty). ## Example request cURL: ```bash curl https://api.serpkite.com/v1/status ``` TypeScript: ```ts const res = await fetch("https://api.serpkite.com/v1/status"); // no key needed const { status, endpoints } = await res.json(); console.log(status, endpoints.map((e) => `${e.endpoint} p95=${e.p95_ms}ms`)); ``` ## Response fields | Field | Type | Description | | --- | --- | --- | | `status` | string | `operational` or `degraded`. | | `window_minutes` | integer | Length of the measurement window (60). | | `updated_at` | string | ISO 8601 timestamp of the snapshot. | | `endpoints[]` | array | `endpoint`, `requests`, `success_rate` (0–1, share without a 5xx), `p50_ms`, `p95_ms`. | ## Example response ```json { "status": "operational", "window_minutes": 60, "updated_at": "2026-09-29T08:00:00Z", "endpoints": [ { "endpoint": "/v1/search", "requests": 18234, "success_rate": 0.994, "p50_ms": 1080, "p95_ms": 2210 }, { "endpoint": "/v1/news", "requests": 2210, "success_rate": 0.997, "p50_ms": 940, "p95_ms": 1890 } ] } ``` ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | A parameter is missing or invalid. | | 401 | `unauthorized` | The API key is missing, invalid or revoked. | | 402 | `insufficient_credits` | Your balance is too low. Buy a pack or wait for the monthly free grant. | | 429 | `rate_limited` | Too many requests per second for your plan. Retry after the Retry-After header. | | 503 | `upstream_error` | Google could not be fetched or parsed. Not billed; retry after Retry-After. | All errors: https://serpkite.com/docs/errors ## Notes - Sample numbers are illustrative. Call the endpoint for live values. ## Related - [Status page](https://serpkite.com/status) --- Source: https://serpkite.com/docs/guides/agents-tool-calling # Web search for agents with tool calling > Give an OpenAI or Anthropic model live Google search by defining a web_search tool backed by SerpKite. Copy-paste tool schemas, full agent loops in Python and TypeScript, and tips to keep token and credit costs down. Function calling (tool use) lets a model decide when it needs fresh information and ask your code to fetch it. This guide defines one tool, `web_search`, that your code implements with a single `POST /v1/search` call returning Markdown. The model reads the Markdown, cites the links, and answers. If your client already supports MCP (Claude Desktop, Claude Code, Cursor, VS Code), you can skip the code and connect the [MCP server](https://serpkite.com/docs/mcp) instead. ## The tool implementation Under the hood every tool call is one search request with `format: "markdown"`, which is far smaller in context than the full JSON (see [Output formats](https://serpkite.com/docs/output-formats)): cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"latest Go release notes","country":"us","format":"markdown"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "latest Go release notes", country: "us", format: "markdown" }); console.log(res); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("latest Go release notes", country="us", format="markdown") print(res) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.SearchMarkdown(ctx, serpkite.SearchParams{Q: "latest Go release notes", Country: "us"}) if err != nil { log.Fatal(err) } fmt.Println(res) } ``` Wrapped as a tool with the [official SDKs](https://serpkite.com/docs/sdks), mapping the model's arguments onto SerpKite parameters: ```python import serpkite from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY def web_search(query: str, country: str = "us", recency: str | None = None) -> str: params = {"country": country, "format": "markdown"} if recency: params["time"] = recency try: return sk.search(query, **params) # a Markdown string except serpkite.SerpKiteError as err: # Hand errors back to the model as text so it can recover or explain. return f"Search failed: {err.message}" ``` ```ts import { SerpKite, SerpKiteError } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY export // webSearch() from "The tool implementation" above, using the serpkite SDK. export async function ask(question: string): Promise { const messages: Anthropic.MessageParam[] = [{ role: "user", content: question }]; for (let round = 0; round < 8; round++) { const response = await client.messages.create({ model: "claude-opus-5-5", max_tokens: 16000, tools: [webSearchTool], messages, }); messages.push({ role: "assistant", content: response.content }); if (response.stop_reason !== "tool_use") { return response.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join(""); } const results: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === "tool_use") { results.push({ type: "tool_result", tool_use_id: block.id, content: await webSearch(block.input as { query: string }), }); } } messages.push({ role: "user", content: results }); } return "Stopped after too many tool calls."; } ``` ## Using an agent framework If you build on LangChain or CrewAI, you don't need to write the tool at all: ```python # LangChain / LangGraph: pip install langchain-serpkite from langchain_serpkite import SerpKiteSearch tool = SerpKiteSearch() # returns Markdown, token-lean, for agents ``` ```python # CrewAI: pip install "serpkite[crewai]" from serpkite.crewai import SerpKiteSearchTool tool = SerpKiteSearchTool() # optional: endpoint="news", country="de", num=10 ``` Pass the tool to your agent like any other. See [SDKs](https://serpkite.com/docs/sdks#langchain) for the retriever and webpage loader. ## Keep it cheap and fast - **Return Markdown or compact JSON.** `format: "markdown"` is the best default for a model. If you post-process results in code first, `format: "compact"` keeps short JSON. See [Output formats](https://serpkite.com/docs/output-formats). - **Project fields.** `fields: "results.title,results.link,results.snippet"` drops everything else from the JSON before it reaches the context. - **Keep `num` at 10.** Most questions are answered from the first page. Offer a second tool (or a `page` argument) rather than fetching 100 results by default. - **Cache repeated queries.** Agents often repeat the same search within a session. Add `max_age: 3600` to accept a result up to an hour old at half the credits. See [Caching](https://serpkite.com/docs/caching). - **Fetch pages only when needed.** A separate `fetch_page` tool backed by [`/v1/webpage`](https://serpkite.com/docs/endpoints/webpage) (`sk.webpage(url)` in the SDKs) lets the model read one promising result instead of paying for `include_content` on every search. - **Cap the blast radius.** Give the agent its own API key with a monthly `credit_limit`. When it is hit, calls return `403 key_limit_reached` and nothing more is charged. See [API keys](https://serpkite.com/docs/api-keys) and [Spend controls](https://serpkite.com/docs/spend-controls). - **Cap the loop.** Limit tool rounds (the examples stop after 8) so a confused model can't search forever. ## Related - [MCP server](https://serpkite.com/docs/mcp): The same search as a hosted MCP server, no code needed. - [RAG pipeline](https://serpkite.com/docs/guides/rag-pipeline): Search, fetch pages, chunk and answer with citations. - [OpenAI Agents SDK](https://serpkite.com/integrations/openai-agents-sdk): Framework-specific setup. - [Vercel AI SDK](https://serpkite.com/integrations/vercel-ai-sdk): A search tool for the AI SDK. --- Source: https://serpkite.com/docs/guides/rag-pipeline # Build a RAG pipeline on live search > Answer questions from the live web. Search Google with SerpKite, get the top pages as clean Markdown in the same call, chunk and rank them, and have an LLM answer with citations. Python and TypeScript code, plus the credit math. Retrieval-augmented generation (RAG) on a fixed document set goes stale. For questions about current events, prices, releases or anything on the open web, retrieve from Google instead. The pipeline is short: 1. **Search** Google for the question. 2. **Fetch** the top result pages as Markdown. 3. **Chunk and rank** the text against the question. 4. **Answer** with an LLM, citing the source URLs. SerpKite does steps 1 and 2 in one request with [`include_content`](https://serpkite.com/docs/include-content). ## Step 1 and 2: search and fetch in one call `include_content: N` fetches the top `N` organic results (0–5) and adds each page's main content as Markdown in `results[].content`: cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"what changed in python 3.14 asyncio","country":"us","num":10,"include_content":3}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "what changed in python 3.14 asyncio", country: "us", num: 10, include_content: 3 }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("what changed in python 3.14 asyncio", country="us", num=10, include_content=3) print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "what changed in python 3.14 asyncio", Country: "us", Num: 10, IncludeContent: 3}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` Cost: 1 credit for the search plus 1 per fetched page, so this call is **4 credits**. Pages that can't be fetched aren't billed; their `content` is simply missing. If you want to choose which pages to read (say, skip forums or pick by domain), search first and then fetch selected URLs with [`POST /v1/webpage`](https://serpkite.com/docs/endpoints/webpage) (`sk.webpage(url)` in the SDKs) at 1 credit each. ## Python This version uses the [SerpKite Python SDK](https://serpkite.com/docs/sdks#python), a simple lexical ranker so there's no vector database to set up, and an LLM for the answer. Swap in embeddings when the corpus grows (see below). ```python import re import anthropic from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY llm = anthropic.Anthropic() # reads ANTHROPIC_API_KEY def retrieve(question: str, pages: int = 3) -> list[dict]: res = sk.search( question, include_content=pages, fields="results.title,results.link,results.snippet,results.content", ) return [r.model_dump() for r in res.results] # pydantic models -> dicts def chunk(text: str, size: int = 1200, overlap: int = 200) -> list[str]: # Split on paragraphs, then pack into ~size-character chunks. paras, chunks, cur = re.split(r"\n{2,}", text), [], "" for p in paras: if len(cur) + len(p) > size and cur: chunks.append(cur) cur = cur[-overlap:] cur += "\n\n" + p if cur.strip(): chunks.append(cur) return chunks def score(question: str, text: str) -> float: terms = {t for t in re.findall(r"\w+", question.lower()) if len(t) > 2} words = re.findall(r"\w+", text.lower()) return sum(w in terms for w in words) / (len(words) ** 0.5 + 1) def answer(question: str) -> str: results = retrieve(question) passages = [] for i, r in enumerate(results, start=1): body = r.get("content") or r.get("snippet", "") for c in chunk(body): passages.append((score(question, c), i, r["link"], c)) top = sorted(passages, reverse=True)[:8] context = "\n\n".join(f"[{i}] {url}\n{text}" for _, i, url, text in top) sources = "\n".join(f"[{i}] {r['title']}: {r['link']}" for i, r in enumerate(results, start=1)) msg = llm.messages.create( model="claude-opus-5-5", max_tokens=16000, system="Answer only from the provided sources. Cite them inline as [n]. If the sources don't answer the question, say so.", messages=[{"role": "user", "content": f"Sources:\n{context}\n\nQuestion: {question}"}], ) text = "".join(b.text for b in msg.content if b.type == "text") return f"{text}\n\nSources:\n{sources}" print(answer("What changed in Python 3.14 asyncio?")) ``` ## TypeScript ```ts import Anthropic from "@anthropic-ai/sdk"; import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const llm = new Anthropic(); // reads ANTHROPIC_API_KEY type Result = { title: string; link: string; snippet?: string; content?: string }; async function retrieve(question: string, pages = 3): Promise { const res = await sk.search({ q: question, include_content: pages, fields: "results.title,results.link,results.snippet,results.content", }); return res.results; } function chunk(text: string, size = 1200): string[] { const out: string[] = []; let cur = ""; for (const p of text.split(/\n{2,}/)) { if (cur.length + p.length > size && cur) { out.push(cur); cur = ""; } cur += `\n\n${p}`; } if (cur.trim()) out.push(cur); return out; } function score(question: string, text: string): number { const terms = new Set(question.toLowerCase().match(/\w{3,}/g) ?? []); const words = text.toLowerCase().match(/\w+/g) ?? []; return words.filter((w) => terms.has(w)).length / (Math.sqrt(words.length) + 1); } export async function answer(question: string): Promise { const results = await retrieve(question); const passages = results.flatMap((r, i) => chunk(r.content ?? r.snippet ?? "").map((text) => ({ n: i + 1, url: r.link, text, s: score(question, text) })), ); const top = passages.sort((a, b) => b.s - a.s).slice(0, 8); const context = top.map((p) => `[${p.n}] ${p.url}\n${p.text}`).join("\n\n"); const msg = await llm.messages.create({ model: "claude-opus-5-5", max_tokens: 16000, system: "Answer only from the provided sources. Cite them inline as [n]. If the sources don't answer the question, say so.", messages: [{ role: "user", content: `Sources:\n${context}\n\nQuestion: ${question}` }], }); const text = msg.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join(""); const sources = results.map((r, i) => `[${i + 1}] ${r.title}: ${r.link}`).join("\n"); return `${text}\n\nSources:\n${sources}`; } ``` ## LangChain With [`langchain-serpkite`](https://serpkite.com/docs/sdks#langchain), steps 1 and 2 are a retriever that returns LangChain `Document`s, with the page Markdown as content: ```python from langchain_serpkite import SerpKiteRetriever retriever = SerpKiteRetriever(k=5, include_content=2) docs = retriever.invoke("What changed in Python 3.14 asyncio?") ``` Plug it into any LangChain chain or LangGraph node. To load specific URLs, use `SerpKiteWebpageLoader(["https://…"]).load()`. ## Using embeddings instead of lexical ranking The lexical ranker above is fine for a handful of pages. For better recall, especially with paraphrased questions: 1. Embed each chunk with the embedding model you already use. 2. Embed the question with the same model. 3. Keep the top 5–10 chunks by cosine similarity, optionally re-ranked with a cross-encoder. With `include_content: 3` you typically get a few dozen chunks per question, small enough to embed on the fly and keep in memory. Persist embeddings only if you answer many questions over the same pages; add `max_age` to the search so repeated questions reuse cached results. ## Credit math | Step | Credits | | --- | --- | | Search (`/v1/search`, 10 results) | 1 | | Page content (`include_content: 3`) | +3 | | **Per question** | **4** | | Same question again within `max_age` (cache hit) | 2 (half of 4) | In general a question costs **1 + N** credits, where N is the number of pages you fetch. At the Pro pack price ($300 for 500,000 credits, $0.60 per 1,000), 4 credits is $0.0024 per question. The `X-Credits-Used` header on each response confirms the actual charge. ## Tips - **Ground the prompt.** Tell the model to answer only from the sources and to say when they don't cover the question. Include the URLs so citations are checkable. - **Localize.** Pass `country` and `language` for country- and language-specific answers. See [Localization](https://serpkite.com/docs/localization). - **Freshness.** Add `time: "week"` (or `day`, `month`) for news-like questions. - **Batch offline work.** If you pre-compute answers for many questions, [`POST /v1/batches`](https://serpkite.com/docs/batch) runs searches at half price. ## Related - [Page content](https://serpkite.com/docs/include-content): How include_content fetches and bills pages. - [Webpage to Markdown](https://serpkite.com/docs/endpoints/webpage): Fetch any public URL as Markdown. - [Tool calling for agents](https://serpkite.com/docs/guides/agents-tool-calling): Let the model decide when to search. - [LlamaIndex](https://serpkite.com/integrations/llamaindex): Use SerpKite as a LlamaIndex retriever. --- Source: https://serpkite.com/docs/guides/rank-tracking # Rank tracking > Track where a domain ranks in Google's top 100 for many keywords, by country, city and device. Uses POST /v1/rank, the num=100 depth bundle (7 credits), POST /v1/batches at half price, and signed webhooks. A rank tracker asks one question per keyword, location and device: at what position does my domain appear? SerpKite gives you the pieces to answer it cheaply at scale: - **`POST /v1/rank`:** send a keyword and a domain, get back the best position and every matching URL in the top 100. - **Depth bundle:** `num: 100` returns the top 100 organic results in one call for **7 credits** instead of 10 pages at 1 credit each. - **Canonical domains:** every result has a `domain` (host without `www.`) and a resolved `link`, so matching doesn't need URL parsing or redirect decoding. - **Batches:** [`POST /v1/batches`](https://serpkite.com/docs/batch) queues up to 100 keywords per call at half price, delivered asynchronously. - **Webhooks:** batch results are POSTed to your endpoint, signed with your webhook secret. ## Find a domain's position [`POST /v1/rank`](https://serpkite.com/docs/endpoints/rank) does the matching for you. It checks the top 100 by default (`num` can be 10, 20, 30, 50 or 100) and is priced like the same search depth, so 7 credits for 100: cURL: ```bash curl https://api.serpkite.com/v1/rank \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"espresso machine","domain":"example.com","country":"us","language":"en"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.rank({ q: "espresso machine", domain: "example.com", country: "us", language: "en" }); console.log(res.position, res.checked); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.rank("espresso machine", "example.com", country="us", language="en") print(res.position, res.checked) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Rank(ctx, serpkite.RankParams{Q: "espresso machine", Domain: "example.com", Country: "us", Language: "en"}) if err != nil { log.Fatal(err) } if res.Position != nil { fmt.Println(*res.Position, res.Checked) } } ``` ```json { "request": { "endpoint": "rank", "engine": "google", "q": "espresso machine", "domain": "example.com", "country": "us", "language": "en", "num": 100 }, "domain": "example.com", "position": 4, "matches": [ { "position": 4, "title": "Espresso Machines | Example", "link": "https://www.example.com/espresso-machines" }, { "position": 37, "title": "Espresso machine guide", "link": "https://shop.example.com/guides/espresso" } ], "checked": 100, "meta": { "request_id": "req_01J8ZK4M6Q2V7", "credits_used": 7, "cached": false } } ``` `position` is the best organic position, or `null` when the domain isn't in the `checked` results. Subdomains match (`shop.example.com` counts for `example.com`), and `matches` lists every URL the domain ranks with. ### Do it yourself from a search If you want the whole top 100 as well (for share of voice or SERP features), call `/v1/search` with `num: 100` and project the fields a tracker needs: cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"espresso machine","country":"us","num":100,"fields":"results.position,results.domain,results.link,results.title"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "espresso machine", country: "us", num: 100, fields: "results.position,results.domain,results.link,results.title" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("espresso machine", country="us", num=100, fields="results.position,results.domain,results.link,results.title") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "espresso machine", Country: "us", Num: 100, Fields: "results.position,results.domain,results.link,results.title"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` Then scan `results`: ```python def position(result: dict, domain: str) -> int | None: domain = domain.removeprefix("www.") for r in result["results"]: if r["domain"] == domain or r["domain"].endswith("." + domain): return r["position"] return None # not in the top 100 ``` ### Depth rules - `num` above 10 works on `/v1/search` and `/v1/news` and requires `page: 1`. - Depth is priced per page of 10 up to the bundle cap: `num: 20` is 2 credits, `num: 50` is 5, and `num: 100` is 7. Pages that come back empty aren't billed. - On other endpoints `num` above 10 is treated as 10. See [Pagination and depth](https://serpkite.com/docs/pagination-and-depth) for the details. ## Location and device Rankings differ by country, city and device, so track each combination separately: | Parameter | Use | | --- | --- | | `country` | Country, e.g. `us`, `de`, `in`. | | `language` | Interface language, e.g. `en`, `de`. | | `location` | City or region, e.g. `"Austin, Texas, United States"`, for local rankings. | | `uule` | A Google-encoded location, if your tool already stores them (search only). | | `device` | `desktop` (default) or `mobile`. Mobile layouts rank differently. | See [Localization](https://serpkite.com/docs/localization) for country and language codes. ## Run it as a batch Daily rank checks don't need an answer in one second. [`POST /v1/batches`](https://serpkite.com/docs/batch) queues search jobs at **half price** and delivers them within minutes (the target is under 15 minutes). Send up to 100 keywords per call: ```bash curl https://api.serpkite.com/v1/batches \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "search", "webhook_url": "https://tracker.example.com/hooks/serpkite", "requests": [ {"q": "espresso machine", "country": "us", "num": 100, "fields": "results.position,results.domain,results.link"}, {"q": "espresso grinder", "country": "us", "num": 100, "fields": "results.position,results.domain,results.link"}, {"q": "dual boiler espresso", "country": "us", "num": 100, "fields": "results.position,results.domain,results.link"} ] }' ``` ```python from serpkite import SerpKite sk = SerpKite() keywords = ["espresso machine", "espresso grinder", "dual boiler espresso"] job = sk.batches.create( endpoint="search", requests=[{"q": k, "country": "us", "num": 100} for k in keywords], webhook_url="https://tracker.example.com/hooks/serpkite", ) ids = {entry.id: k for entry, k in zip(job.batches, keywords) if getattr(entry, "id", None)} ``` The API answers `202 Accepted` with a `batches` array in the same order as your requests; an entry is a job, or an `error` object for a request that was invalid or couldn't be queued: ```json { "batches": [ { "id": "0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "status": "queued", "endpoint": "/v1/search", "created_at": "2026-09-29T06:00:00Z", "poll_url": "https://api.serpkite.com/v1/batches/0192f7a4-6c1e-7b3a-9d2f-5e8a1c4b7d90", "webhook_url": "https://tracker.example.com/hooks/serpkite", "webhook_status": "pending", "completed_at": null, "credits_used": 0 } ] } ``` Store each `id` next to the keyword it belongs to. When a job finishes, SerpKite POSTs a `batch.completed` event with the full search `result` to the webhook URL. If you omit `webhook_url`, the account's default webhook from the dashboard settings is used; with neither, poll [`GET /v1/batches/{id}`](https://serpkite.com/docs/endpoints/batches) or call `sk.batches.wait(id)` in the SDKs. Results are kept for 24 hours. ### Receive and verify the webhook Each delivery carries `X-SerpKite-Timestamp` and `X-SerpKite-Signature: v1=`, an HMAC-SHA256 of `.` with your webhook secret. Verify it before trusting the payload: ```python import hashlib import hmac import time def verify(secret: str, timestamp: str, signature: str, body: bytes) -> bool: if abs(time.time() - int(timestamp)) > 300: # reject stale deliveries return False expected = "v1=" + hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) ``` Answer with any `2xx` quickly and process the result asynchronously; failed deliveries are retried with backoff. Deliveries can arrive more than once, so deduplicate on the job `id` (also sent as `X-SerpKite-Delivery`). The full payload format, retry schedule and verification code in more languages are in [Webhooks](https://serpkite.com/docs/webhooks). ### Scheduling tips - Spread a large daily run over the day rather than submitting everything at midnight. There is a cap on how many jobs an account can have queued at once (10,000 by default); beyond it, requests get `429 rate_limited`. - Each job is billed on its own: 3.5 credits for a `num: 100` batch search. Failed and empty searches are refunded. - An `insufficient_credits` error for one entry doesn't fail the others that fit your balance, so check every entry of the `batches` array. ## What it costs For 1,000 keywords checked daily in one location on desktop, top 100, as batch jobs: | | | | --- | --- | | Per keyword per day | 7 credits × 0.5 = **3.5 credits** | | Per day | 1,000 × 3.5 = **3,500 credits** | | Per 30-day month | **105,000 credits** | | At Pro pack price ($0.60 per 1,000) | about **$63 per month** | Add a location or device and the cost multiplies accordingly (desktop and mobile is 7,000 credits a day). Realtime `/v1/rank` or `/v1/search` at the same depth costs 7 credits per keyword, twice the batch price. Credits never expire, so buying a larger pack for its lower per-credit price doesn't waste money if usage fluctuates. Tracking far more keywords than this? Volumes above the largest pack are quoted on request; see [Pricing](https://serpkite.com/pricing) and [Enterprise](https://serpkite.com/enterprise). ## Beyond position A full search response gives you more than rank: - **SERP features:** check whether the query shows an `answer_box`, `people_also_ask`, `top_stories` or `places` (local pack). Add them to `fields` when you track them. - **Competitors:** store the full top 10 or top 100 domains per keyword to chart share of voice. - **Changes:** `meta.parse_quality` is `ok` for a clean parse; treat `partial` results with care before alerting on a rank drop. ## Related - [Rank endpoint](https://serpkite.com/docs/endpoints/rank): Request and response reference for POST /v1/rank. - [Free rank checker](https://serpkite.com/tools/rank-checker): Check a domain's position for one keyword in the browser. - [Batch requests](https://serpkite.com/docs/batch): POST /v1/batches, polling and pricing. - [Webhooks](https://serpkite.com/docs/webhooks): Payload, retries and signature verification. --- Source: https://serpkite.com/docs/guides/migrate-from-google-cse # Migrate from Google Custom Search > Google shuts down the Custom Search JSON API on January 1, 2027. SerpKite's GET /customsearch/v1 takes the same parameters and returns the same response shape, so you change the host and the key and keep your parser. Google is retiring the Custom Search JSON API (`https://www.googleapis.com/customsearch/v1`) on **January 1, 2027**. SerpKite ships a compatible endpoint, [`GET /customsearch/v1`](https://serpkite.com/docs/endpoints/customsearch), that accepts the same query parameters and returns the same `customsearch#search` document. Code that reads `items[].title`, `items[].link`, `searchInformation.totalResults` or `queries.nextPage` keeps working. ## The change ```diff - https://www.googleapis.com/customsearch/v1?key=GOOGLE_KEY&cx=ENGINE_ID&q=asyncio + https://api.serpkite.com/customsearch/v1?key=skt_live_...&cx=ENGINE_ID&q=asyncio ``` 1. Replace the host `www.googleapis.com` (or `customsearch.googleapis.com`) with `api.serpkite.com`. The path stays `/customsearch/v1`. 2. Replace the Google API key with a SerpKite key. You can keep passing it as `key=`, or move it to an `Authorization: Bearer` header so it stays out of URLs and logs. 3. Leave `cx` as it is. It is accepted and ignored. Paste an existing request URL into the [CSE migration helper](https://serpkite.com/tools/cse-migration) to get the rewritten URL and code. ## Parameters | Parameter | Supported | Notes | | --- | --- | --- | | `q` | Yes | Required. | | `key` | Yes | Your SerpKite key. `Authorization: Bearer` also works. | | `cx` | Accepted, ignored | Results come from the whole web, not a Programmable Search Engine. | | `start` | Yes | 1–91, in steps of 10 (1, 11, 21…). | | `num` | Yes | 1–10. | | `gl` | Yes | Country code. | | `hl` | Yes | Interface language. | | `lr` | Yes | Language restrict, e.g. `lang_de`. | | `safe` | Yes | `active` or `off`. | | `dateRestrict` | Yes | `d7`, `w2`, `m6`, `y1`… | | `siteSearch` | Yes | Restrict to one site. | | `searchType` | Yes | `image` for image results. | > **If your engine was restricted to certain sites** > A Programmable Search Engine could be limited to a list of sites. SerpKite ignores `cx`, so add the restriction to the request: `siteSearch=docs.example.com`, or a `site:` operator in `q` (`site:docs.example.com OR site:blog.example.com asyncio`). ## Response The response keeps the CSE shape: `kind`, `searchInformation`, `items[]` (with `title`, `htmlTitle`, `link`, `displayLink`, `snippet`, `htmlSnippet`, `formattedUrl`, `pagemap`, `image` for image search) and `queries` with `request` and `nextPage`. GET /customsearch/v1 · illustrative: ```json { "kind": "customsearch#search", "searchInformation": { "searchTime": 0.94, "formattedSearchTime": "0.94", "totalResults": "1240000", "formattedTotalResults": "1,240,000" }, "items": [ { "kind": "customsearch#result", "title": "asyncio — Asynchronous I/O", "htmlTitle": "asyncio — Asynchronous I/O", "link": "https://docs.python.org/3/library/asyncio.html", "displayLink": "docs.python.org", "snippet": "asyncio is a library to write concurrent code using the async/await syntax." } ], "queries": { "request": [ { "searchTerms": "site:docs.python.org asyncio", "count": 10, "startIndex": 1 } ] } } ``` Two differences to handle: - **Errors** use SerpKite's shape, `{"error":{"code","message","request_id"}}`, not Google's `{"error":{"code":403,"errors":[…]}}`. Google put the HTTP status in `error.code`; SerpKite puts a string such as `unauthorized` or `insufficient_credits` there. See [Errors](https://serpkite.com/docs/errors). - **Quota.** There is no 100-queries-per-day free cap and no 10,000-per-day ceiling. You pay 1 credit per call from a prepaid balance, and your plan sets a requests-per-second [rate limit](https://serpkite.com/docs/rate-limits). ## Code ### Python with requests ```python import os import requests res = requests.get( "https://api.serpkite.com/customsearch/v1", params={"q": "site:docs.python.org asyncio", "cx": "any", "num": 10, "start": 1}, headers={"Authorization": f"Bearer {os.environ['SERPKITE_API_KEY']}"}, timeout=30, ) res.raise_for_status() data = res.json() print(data["searchInformation"]["totalResults"]) for item in data.get("items", []): print(item["title"], item["link"]) ``` ### Python with google-api-python-client If you use Google's client library, point it at SerpKite with `client_options`. The library builds URLs from the discovery document's root, so overriding the endpoint sends requests to `https://api.serpkite.com/customsearch/v1`: ```python import os from googleapiclient.discovery import build service = build( "customsearch", "v1", developerKey=os.environ["SERPKITE_API_KEY"], client_options={"api_endpoint": "https://api.serpkite.com/"}, ) data = service.cse().list(q="site:docs.python.org asyncio", cx="any").execute() for item in data.get("items", []): print(item["title"], item["link"]) ``` The discovery document itself is still fetched from Google, so this path depends on Google's discovery service staying up after the shutdown. For a long-lived migration, prefer the plain `requests` version above. ### Node.js ```js const url = new URL("https://api.serpkite.com/customsearch/v1"); url.search = new URLSearchParams({ q: "site:docs.python.org asyncio", cx: "any", num: "10" }); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SERPKITE_API_KEY}` }, }); if (!res.ok) throw new Error((await res.json()).error.message); const { items = [], searchInformation } = await res.json(); console.log(searchInformation.totalResults, items.map((i) => i.link)); ``` ### Go ```go q := url.Values{} q.Set("q", "site:docs.python.org asyncio") q.Set("cx", "any") q.Set("num", "10") req, _ := http.NewRequest("GET", "https://api.serpkite.com/customsearch/v1?"+q.Encode(), nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("SERPKITE_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var out struct { Items []struct { Title string `json:"title"` Link string `json:"link"` } `json:"items"` } if err := json.NewDecoder(res.Body).Decode(&out); err != nil { log.Fatal(err) } for _, it := range out.Items { fmt.Println(it.Title, it.Link) } ``` ## Pagination As with CSE, page with `start`: `start=1` is results 1–10, `start=11` is 11–20, up to `start=91`. Each call costs 1 credit. If you need the top 100 in one go, the native [`/v1/search`](https://serpkite.com/docs/endpoints/search) endpoint with `num: 100` returns them in one call for 7 credits instead of 10 (see [Pagination and depth](https://serpkite.com/docs/pagination-and-depth)). ## Going further than CSE Once you're on SerpKite, the native endpoints give you more than CSE ever returned: People Also Ask, the knowledge graph, the answer box, news, maps and more, plus [Markdown output](https://serpkite.com/docs/output-formats) for LLMs. The mapping from CSE is straightforward: `items[].link` is `results[].link`, `items[].title` is `results[].title`, `items[].snippet` is `results[].snippet`, `start` becomes `page`, and the CSE `gl` and `hl` parameters become `country` and `language`. With the [official SDKs](https://serpkite.com/docs/sdks): cURL: ```bash curl https://api.serpkite.com/v1/search \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q":"site:docs.python.org asyncio","country":"us","language":"en"}' ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "site:docs.python.org asyncio", country: "us", language: "en" }); console.log(res.results[0].title, res.meta.credits_used); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("site:docs.python.org asyncio", country="us", language="en") print(res.results[0].title, res.meta.credits_used) ``` Go: ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "site:docs.python.org asyncio", Country: "us", Language: "en"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) } ``` ## Related - [CSE shutdown and alternatives](https://serpkite.com/migrate/google-custom-search-api): What is changing, the timeline and your options. - [CSE migration helper](https://serpkite.com/tools/cse-migration): Paste a CSE URL, get the SerpKite equivalent. - [Custom Search reference](https://serpkite.com/docs/endpoints/customsearch): Every parameter and response field. - [Authentication](https://serpkite.com/docs/authentication): Header and query-string keys. --- Source: https://serpkite.com/docs/mcp # MCP server > Give Claude, Cursor, VS Code, ChatGPT or any MCP client live Google search through SerpKite's remote MCP server at https://api.serpkite.com/v1/mcp. One URL, your API key, eleven tools. SerpKite runs a remote [Model Context Protocol](https://modelcontextprotocol.io) server. Any MCP client that speaks streamable HTTP can connect to it and call Google search, News, Maps, Scholar, a page fetcher and more as tools. There is nothing to install or host. | | | | --- | --- | | URL | `https://api.serpkite.com/v1/mcp` | | Transport | Streamable HTTP (stateless) | | Auth | `Authorization: Bearer skt_live_…` | | Protocol versions | `2025-06-18`, `2025-03-26`, `2024-11-05` | | Billing | Same credits as the REST endpoints | > **OAuth is planned** > Today the server authenticates with your API key in a header. OAuth sign-in for clients that can't send custom headers is planned. Until then, use a client that lets you set headers, or the `mcp-remote` bridge shown below. ## Get a key Create a key in the dashboard under **API keys** (see [API keys](https://serpkite.com/docs/api-keys)). For MCP use, a dedicated key with a monthly credit limit is a good idea: an agent in a loop can make many calls, and the limit caps what that key can spend. The examples below read the key from the `SERPKITE_API_KEY` environment variable. ## Claude Code One command adds the server to Claude Code: ```bash claude mcp add --transport http serpkite https://api.serpkite.com/v1/mcp \ --header "Authorization: Bearer $SERPKITE_API_KEY" ``` Run `claude mcp list` to check the connection, then ask Claude something that needs fresh results ("what changed in the latest Go release?"). Add `--scope project` to write the config to `.mcp.json` so your whole team gets it, but keep the key itself out of version control. ## Claude Desktop Claude Desktop launches local (stdio) servers from `claude_desktop_config.json`. To reach a remote server with a custom header, use the `mcp-remote` bridge, which runs through `npx`: ```json { "mcpServers": { "serpkite": { "command": "npx", "args": [ "-y", "mcp-remote", "https://api.serpkite.com/v1/mcp", "--header", "Authorization: Bearer ${SERPKITE_API_KEY}" ], "env": { "SERPKITE_API_KEY": "skt_live_..." } } } } ``` The file lives at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS and `%APPDATA%\Claude\claude_desktop_config.json` on Windows. Restart Claude Desktop after editing it. You need Node.js installed for `npx`. Where your plan offers **Settings → Connectors → Add custom connector**, you can add the URL there instead. Custom connectors that need a header will work without the bridge once OAuth ships. ## Cursor Add the server to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project): ```json { "mcpServers": { "serpkite": { "url": "https://api.serpkite.com/v1/mcp", "headers": { "Authorization": "Bearer ${env:SERPKITE_API_KEY}" } } } } ``` Open **Cursor Settings → MCP** to check that the server is green and its tools are listed. If your Cursor version doesn't expand `${env:…}`, paste the key directly and keep the file out of git. ## VS Code VS Code (Copilot agent mode) reads `.vscode/mcp.json`. The `inputs` block prompts for the key once and stores it securely, so it never lands in the file: ```json { "inputs": [ { "type": "promptString", "id": "serpkite-key", "description": "SerpKite API key", "password": true } ], "servers": { "serpkite": { "type": "http", "url": "https://api.serpkite.com/v1/mcp", "headers": { "Authorization": "Bearer ${input:serpkite-key}" } } } } ``` Start the server from the **MCP: List Servers** command, then pick the SerpKite tools in the agent tool picker. ## ChatGPT ChatGPT can connect remote MCP servers as connectors when developer mode is enabled for your workspace. Be aware that ChatGPT's connector setup authenticates with OAuth or no auth, and may not let you add a custom `Authorization` header. Until SerpKite's OAuth support ships, ChatGPT is the one client on this page that may not be able to connect directly. For OpenAI models in your own code, use the [tool calling guide](https://serpkite.com/docs/guides/agents-tool-calling) instead, or the OpenAI Agents SDK, which accepts MCP server headers. ## Other clients Any client that supports streamable HTTP with custom headers works with the same two values: the URL and the `Authorization` header. Clients that only support stdio can use `npx -y mcp-remote https://api.serpkite.com/v1/mcp --header "Authorization: Bearer …"` as the command, as in the Claude Desktop example. ## Tools All tools return LLM-ready Markdown (the same output as `format: "markdown"` on the REST API) and cost the same credits as the matching endpoint. | Tool | What it does | Inputs | Credits | | --- | --- | --- | --- | | `search` | Google web search: organic results, answer box, knowledge graph, People Also Ask, related searches | `q`, `country`, `language`, `location`, `page`, `time`, `num` (10, 20, 30, 50, 100), `include_content` (0–5) | 1 per page, up to 7 for `num: 100`, +1 per fetched page | | `news` | Google News articles | `q`, `country`, `language`, `location`, `page`, `time` | 1 | | `maps` | Places with address, rating, phone, website, coordinates | `q`, `country`, `language`, `location`, `page` | 1 | | `scholar` | Academic papers with citations and PDF links | `q`, `country`, `language`, `location`, `page` | 1 | | `patents` | Patent search | `q`, `country`, `language`, `location`, `page` | 1 | | `shopping` | Products with prices and merchants | `q`, `country`, `language`, `location`, `page` | 1 | | `images` | Image search | `q`, `country`, `language`, `location`, `page` | 1 | | `videos` | Video search | `q`, `country`, `language`, `location`, `page` | 1 | | `autocomplete` | Query suggestions | `q`, `country`, `language`, `location`, `page` | 0.5 | | `webpage` | Fetch a public URL and return its main content as Markdown with metadata | `url` | 1 | `q` is required on every tool except `webpage`, which requires `url`. `time` is one of `hour`, `day`, `week`, `month`, `year`. As with the REST API, failed and empty calls are not billed. ## Test it with curl The server is stateless: every `POST` carries one JSON-RPC 2.0 message (or a batch of up to 20) and gets a JSON reply. No session setup is needed, which makes it easy to test by hand. Initialize: ```bash curl https://api.serpkite.com/v1/mcp \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' ``` List the tools: ```bash curl https://api.serpkite.com/v1/mcp \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' ``` Call `search`: ```bash curl https://api.serpkite.com/v1/mcp \ -H "Authorization: Bearer $SERPKITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search","arguments":{"q":"best espresso machine 2026","country":"us"}}}' ``` The result's `content` array holds a `text` item with the Markdown. Notifications (messages without an `id`) get an empty `202 Accepted`. A missing or invalid key returns `401` with the usual [error body](https://serpkite.com/docs/errors). ## Costs and safety - Every tool call is a billed API request, visible in the dashboard request log like any other call. - Give the MCP key a monthly `credit_limit` so a runaway agent loop stops at a known cost. When the limit is hit, calls fail with `key_limit_reached` and nothing more is charged. See [Spend controls](https://serpkite.com/docs/spend-controls). - The server only fetches public, logged-out pages. The `webpage` tool refuses private network addresses. ## Related - [MCP integration overview](https://serpkite.com/integrations/mcp): Setup walkthroughs and use cases for Claude, Cursor and ChatGPT. - [Tool calling without MCP](https://serpkite.com/docs/guides/agents-tool-calling): Define a search tool directly for OpenAI and Anthropic models. - [Output formats](https://serpkite.com/docs/output-formats): What the Markdown the tools return looks like. - [API keys](https://serpkite.com/docs/api-keys): Create a dedicated key with a monthly limit. --- Source: https://serpkite.com/docs/sdks # Official SDKs > Official SerpKite SDKs for TypeScript, Python (sync and async) and Go, plus native LangChain and CrewAI packages. Install, quickstart, options, errors and batches for each. SerpKite ships official, typed clients for the three languages most agents are written in, and native packages for the two most-used agent frameworks. They are generated from the [OpenAPI spec](https://serpkite.com/docs/openapi), so they track the v1 API exactly, and each one is open source in its own repository under [github.com/serpkite](https://github.com/serpkite). | Package | Install | Source | | --- | --- | --- | | TypeScript / JavaScript | `npm install serpkite` | [serpkite/serpkite-js](https://github.com/serpkite/serpkite-js) | | Python (sync + async) | `pip install serpkite` | [serpkite/serpkite-python](https://github.com/serpkite/serpkite-python) | | Go | `go get github.com/serpkite/serpkite-go` | [serpkite/serpkite-go](https://github.com/serpkite/serpkite-go) | | LangChain | `pip install langchain-serpkite` | [serpkite/langchain-serpkite](https://github.com/serpkite/langchain-serpkite) | | CrewAI | `pip install "serpkite[crewai]"` | [serpkite/serpkite-python](https://github.com/serpkite/serpkite-python) (the `serpkite.crewai` module) | Every SDK reads the key from the `SERPKITE_API_KEY` environment variable by default, sends it as `Authorization: Bearer`, retries `429` and `5xx` responses with backoff, and returns the same `results` / `meta` envelope the HTTP API does. ```bash export SERPKITE_API_KEY="skt_live_..." ``` ## TypeScript Zero dependencies. Works on Node.js 18+, Bun, Deno and edge runtimes (Cloudflare Workers, Vercel Edge). ```bash npm install serpkite ``` ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); // reads SERPKITE_API_KEY const res = await sk.search({ q: "best espresso machine", country: "us" }); console.log(res.results[0].title, res.meta.credits_used); // Markdown for an LLM context window: returns a string const md = await sk.search({ q: "best espresso machine", format: "markdown" }); ``` Options: `new SerpKite({ apiKey, baseUrl, timeoutMs, maxRetries })`. Every endpoint has a method: `sk.search`, `sk.images`, `sk.videos`, `sk.news`, `sk.maps`, `sk.places`, `sk.reviews`, `sk.shopping`, `sk.scholar`, `sk.patents`, `sk.autocomplete`, `sk.lens`, `sk.webpage`, `sk.rank` and `sk.account`. Parameters use the API's snake_case names (`country`, `language`, `include_content`, `place_id`…). Batches and errors: ```ts import { SerpKite, SerpKiteError } from "serpkite"; const sk = new SerpKite(); const { batches } = await sk.batches.create({ endpoint: "search", requests: [{ q: "espresso grinder" }, { q: "burr grinder" }], }); const done = await sk.batches.wait(batches[0].id); // polls until done or failed try { await sk.search({ q: "" }); } catch (err) { if (err instanceof SerpKiteError) console.error(err.status, err.code, err.message, err.requestId); } ``` The API sends `Access-Control-Allow-Origin: *`, so the SDK also works in a browser. Don't ship a production key to browsers; proxy through your backend or use a key with a low monthly limit. ## Python Built on `httpx` and pydantic v2, fully typed, Python 3.9+. `SerpKite` is synchronous; `AsyncSerpKite` has the same methods for asyncio. ```bash pip install serpkite ``` ```python from serpkite import SerpKite sk = SerpKite() # reads SERPKITE_API_KEY res = sk.search("best espresso machine", country="us") print(res.results[0].title, res.meta.credits_used) md = sk.search("best espresso machine", format="markdown") # str ``` Options: `SerpKite(api_key=..., base_url=..., timeout=..., max_retries=...)`. Methods: `sk.search`, `sk.images`, `sk.videos`, `sk.news`, `sk.maps`, `sk.places`, `sk.reviews(place_id=...)`, `sk.shopping`, `sk.scholar`, `sk.patents`, `sk.autocomplete`, `sk.lens(url)`, `sk.webpage(url)`, `sk.rank(q, domain)` and `sk.account()`. Async, running several queries concurrently: ```python import asyncio from serpkite import AsyncSerpKite async def main() -> None: sk = AsyncSerpKite() queries = ["vector database", "rag evaluation", "mcp server"] pages = await asyncio.gather(*(sk.search(q, format="compact") for q in queries)) for q, page in zip(queries, pages): print(q, page.results[0].link) asyncio.run(main()) ``` Batches and errors: ```python import serpkite from serpkite import SerpKite sk = SerpKite() job = sk.batches.create(endpoint="search", requests=[{"q": "espresso grinder"}, {"q": "burr grinder"}]) done = sk.batches.wait(job.batches[0].id) try: sk.search("") except serpkite.SerpKiteError as err: print(err.status, err.code, err.message, err.request_id) ``` ## Go Package `serpkite`, module `github.com/serpkite/serpkite-go`. ```bash go get github.com/serpkite/serpkite-go ``` ```go package main import ( "context" "fmt" "log" serpkite "github.com/serpkite/serpkite-go" ) func main() { ctx := context.Background() c := serpkite.NewClient() // reads SERPKITE_API_KEY res, err := c.Search(ctx, serpkite.SearchParams{Q: "best espresso machine", Country: "us"}) if err != nil { log.Fatal(err) } fmt.Println(res.Results[0].Title, res.Meta.CreditsUsed) md, err := c.SearchMarkdown(ctx, serpkite.SearchParams{Q: "best espresso machine"}) if err != nil { log.Fatal(err) } fmt.Println(md) } ``` Options: `serpkite.NewClient(serpkite.WithAPIKey(key), serpkite.WithBaseURL(url), serpkite.WithHTTPClient(hc), serpkite.WithMaxRetries(n))`. Methods: `c.Search`, `c.Images`, `c.Videos`, `c.News`, `c.Maps`, `c.Places`, `c.Reviews`, `c.Shopping`, `c.Scholar`, `c.Patents`, `c.Autocomplete`, `c.Lens`, `c.Webpage`, `c.Rank`, `c.Account`, and `c.Batches.Create`, `c.Batches.Get`, `c.Batches.Wait`. Errors are returned as `*serpkite.Error` with `Status`, `Code`, `Message` and `RequestID`: ```go var apiErr *serpkite.Error if errors.As(err, &apiErr) && apiErr.Code == "insufficient_credits" { // top up at https://app.serpkite.com/billing } ``` ## LangChain `langchain-serpkite` provides tools, a retriever and a document loader for LangChain and LangGraph. ```bash pip install langchain-serpkite ``` ```python from langchain_serpkite import ( SerpKiteAPIWrapper, SerpKiteRetriever, SerpKiteSearch, SerpKiteSearchResults, SerpKiteWebpageLoader, ) tool = SerpKiteSearch() # returns Markdown, token-lean, for agents results = SerpKiteSearchResults() # returns a JSON list of results retriever = SerpKiteRetriever(k=5, include_content=2) # -> list[Document] docs = SerpKiteWebpageLoader(["https://example.com"]).load() ``` Pass `tool` or `results` to any agent (`create_react_agent`, LangGraph tool nodes). `SerpKiteRetriever` returns the top results as `Document`s, with the page Markdown as content when `include_content` is set. More in the [LangChain integration](https://serpkite.com/integrations/langchain). ## CrewAI The CrewAI tool ships as an extra of the Python SDK. ```bash pip install "serpkite[crewai]" ``` ```python from crewai import Agent from serpkite.crewai import SerpKiteSearchTool tool = SerpKiteSearchTool() # optional: endpoint="news", country="de", num=10 agent = Agent( role="Researcher", goal="Find current, sourced facts", backstory="You check the live web before answering.", tools=[tool], ) ``` More in the [CrewAI integration](https://serpkite.com/integrations/crewai). ## MCP For Claude, Cursor, VS Code and other MCP clients you don't need an SDK at all: connect the remote MCP server at `https://api.serpkite.com/v1/mcp` with your key in an `Authorization: Bearer` header. See [MCP server](https://serpkite.com/docs/mcp). ## Other languages Every language with an HTTP client can call SerpKite directly: 1. `POST https://api.serpkite.com/v1/` with a JSON object body and `Content-Type: application/json`. 2. Send the key as `Authorization: Bearer skt_live_…`. 3. On a non-2xx status, read `error.code` from the body and retry only `429`, `5xx` and timeouts. To generate a typed client, feed the [OpenAPI 3.1 spec](https://serpkite.com/openapi/serp-api.yaml) to your generator of choice (openapi-generator, oapi-codegen, openapi-typescript…). See [OpenAPI spec](https://serpkite.com/docs/openapi). Other frameworks and automation tools have their own pages: [LlamaIndex](https://serpkite.com/integrations/llamaindex), [Vercel AI SDK](https://serpkite.com/integrations/vercel-ai-sdk), [OpenAI Agents SDK](https://serpkite.com/integrations/openai-agents-sdk), [n8n](https://serpkite.com/integrations/n8n) and more under [Integrations](https://serpkite.com/integrations). --- Source: https://serpkite.com/docs/openapi # OpenAPI spec > The SerpKite SERP API is described by an OpenAPI 3.1 document. Download it to generate typed clients, import the API into Postman, Insomnia or Bruno, or give it to an LLM. The contract for `api.serpkite.com` is a single OpenAPI 3.1 file. It is the source of truth the API is built from: when an endpoint or field changes, this file changes first. - **Download:** [https://serpkite.com/openapi/serp-api.yaml](https://serpkite.com/openapi/serp-api.yaml) - **Format:** OpenAPI 3.1.0, YAML - **Version:** 1.0.0 ```bash curl -O https://serpkite.com/openapi/serp-api.yaml ``` ## What's in it - **Servers:** `https://api.serpkite.com`. - **Security scheme:** `bearer` (`Authorization: Bearer skt_live_…`). GET requests may also pass `?api_key=`. - **Every endpoint:** `/v1/search`, `/v1/images`, `/v1/videos`, `/v1/news`, `/v1/maps`, `/v1/places`, `/v1/reviews`, `/v1/shopping`, `/v1/scholar`, `/v1/patents`, `/v1/autocomplete`, `/v1/lens`, `/v1/webpage`, `/v1/rank`, `POST /v1/batches`, `/v1/batches/{id}`, `/v1/account`, `/v1/status`, `/customsearch/v1`, plus the no-login `/playground/search` demo and the `/v1/mcp` endpoint. - **Request schemas:** `SearchRequest` (shared by the search verticals), `ReviewsRequest`, `LensRequest`, `WebpageRequest`, `RankRequest`, `BatchCreateRequest`. - **Response schemas:** one per vertical (`SearchResponse`, `NewsResponse`, `PlacesResponse`…), all sharing the `request` / `results` / `meta` envelope (`RequestEcho`, `Meta`), plus `RankResponse`, `Batch`, `BatchCreateResponse`, `Account`, `Status`, `CSEResponse` and the `Error` shape. - **Headers:** `X-Credits-Used`, `X-Credits-Remaining`, `X-Cost-USD`, `X-Cache`, `X-Latency-Ms` and `X-Tokens-Estimate` on every billed response. - **Alternate content types:** `text/markdown` responses for `format: "markdown"`. The dashboard backend (`app-api.serpkite.com`) is an internal API for the SerpKite dashboard and is not part of the public contract. ## Generate a client For TypeScript, Python and Go, use the [official SDKs](https://serpkite.com/docs/sdks), which are generated from this spec and hand-polished. For other languages, or if you want your own types, generators produce good clients from the spec. TypeScript types with [openapi-typescript](https://openapi-ts.dev), used with `openapi-fetch`: ```bash npx openapi-typescript https://serpkite.com/openapi/serp-api.yaml -o src/serpkite.d.ts ``` Go with [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen): ```bash go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest curl -sO https://serpkite.com/openapi/serp-api.yaml oapi-codegen -generate types,client -package serpkite serp-api.yaml > serpkite/client.gen.go ``` Python, Java, C#, Ruby, PHP and more with [OpenAPI Generator](https://openapi-generator.tech): ```bash npx @openapitools/openapi-generator-cli generate \ -i https://serpkite.com/openapi/serp-api.yaml \ -g python \ -o ./serpkite-client ``` Some generators struggle with OpenAPI 3.1 `oneOf` unions, such as the job-or-error entries of `BatchCreateResponse` or the engine-or-list `engine` parameter. If one does, keep the generated models and send requests with your own thin wrapper. ## Import into an API client - **Postman:** **Import → Link**, paste `https://serpkite.com/openapi/serp-api.yaml`. Set the collection's auth to **Bearer Token** with your key. - **Insomnia:** **Create → Import → From URL**, paste the same URL. - **Bruno:** **Import Collection → OpenAPI V3 Spec**, choose the downloaded file. Keep the key in an environment variable of the tool, not in the saved requests you share. ## Use it with an LLM The spec is compact enough to paste into a model's context when you ask it to write integration code. For the prose docs as well, use [llms.txt](https://serpkite.com/llms.txt) (an index) or [llms-full.txt](https://serpkite.com/llms-full.txt) (every docs page as Markdown), or add `.md` to any docs URL. ## Versioning The API is at version 1.0.0. Additive changes (new endpoints, new optional parameters, new response fields) ship without a version bump, so parse responses tolerantly and ignore unknown fields. Breaking changes will get a new version and advance notice in the [API changelog](https://serpkite.com/docs/changelog). --- Source: https://serpkite.com/docs/changelog # API changelog > Changes to the SerpKite API contract, newest first. Additive changes ship without notice; anything breaking is announced here in advance. This page tracks the public API at `api.serpkite.com`: endpoints, parameters, response fields, errors and billing rules. For changes to Google's results pages themselves, see the [Google SERP changelog](https://serpkite.com/google-serp-changelog). **Compatibility policy.** New endpoints, optional parameters and response fields are additive and can appear at any time, so ignore fields you don't recognize. Removing or renaming anything, or changing what a call costs, is a breaking change: it will get advance notice here and a new API version. ## 2026-10-02: AI Overview and AI Mode removed ### Removed - The `ai_overview` request parameter and response field on `/v1/search` (including `format=compact` and `format=markdown` output). Sending `ai_overview` now returns `400 invalid_request` ("AI Overviews are no longer returned; remove it"). - The `POST /v1/ai-mode` endpoint. - The `ai_overview` tool on the remote MCP server. Everything else on `/v1/search` (organic results, answer box, knowledge graph, People Also Ask, related searches, top stories, local pack and ads) and every other endpoint is unchanged, as are the credit prices. ## v1.0.0 (2026-09-29): the v1 API SerpKite's own versioned API, published as an [OpenAPI 3.1 spec](https://serpkite.com/openapi/serp-api.yaml). It replaces the unversioned preview routes. ### Breaking changes from the preview - **Paths:** every route is under `/v1`: `/v1/search`, `/v1/news`, `/v1/images`, `/v1/videos`, `/v1/maps`, `/v1/places`, `/v1/reviews`, `/v1/shopping`, `/v1/scholar`, `/v1/patents`, `/v1/autocomplete`, `/v1/lens`, `/v1/ai-mode`, `/v1/webpage`, `/v1/rank`, `/v1/account`, `/v1/status`, `/v1/batches`. `GET /customsearch/v1` is unchanged. - **Auth:** `Authorization: Bearer skt_live_…` (or `?api_key=` on GET). The legacy key header is no longer accepted. - **Parameters:** `country` and `language` set the market and interface language. Reviews take `place_id` (or `cid`/`fid`), `sort` and `page_token`. The endpoint is chosen by the path, not a body field. - **Strict validation:** unknown parameters return `400 invalid_request`, and the message names the replacement (for example `unknown parameter "type": use the endpoint path`). - **Response envelope:** every vertical returns `request` (the normalised request), `results` (its primary list: organic rows, images, news, places, reviews, papers, suggestions…), vertical extras and `meta`. All keys are snake_case (`ai_overview`, `knowledge_graph`, `people_also_ask`, `related_searches`, `displayed_link`, `image_url`…). `fields` paths follow suit: `results.title,results.link,ai_overview`. - **Batches:** `POST /v1/batches` with `{"endpoint", "requests": [1–100 bodies], "webhook_url"}` queues one half-price job per request. Realtime endpoints take a single JSON object; array bodies and the batch mode flag are gone. - **MCP:** the remote server is at `https://api.serpkite.com/v1/mcp`, with Bearer auth; tool arguments use `country` and `language`. ### New - `engine` parameter on `/v1/search`, `/v1/news`, `/v1/images` and `/v1/videos`: `google` (the default, unchanged), `auto`, one provider or a list. Opt-in fallback to other search engines when Google is unavailable, labelled in `meta.engine` and the new `meta.route`, plus a per-key dashboard setting. See [Search providers](https://serpkite.com/docs/providers). - `POST /v1/rank`: the best position of a domain for a keyword in the top 10 to 100. - `GET /v1/status`: public per-endpoint success rate and latency over the last hour. - Official SDKs: TypeScript (`npm install serpkite`), Python (`pip install serpkite`, sync and async) and Go (`github.com/serpkite/serpkite-go`), plus `langchain-serpkite` and `serpkite[crewai]`. See [SDKs](https://serpkite.com/docs/sdks). ### Unchanged - `format` (`json`, `compact`, `markdown`), `fields`, `include_content`, `num: 100` depth bundle, `max_age` caching, the AI Overview at no extra cost, resolved destination URLs and the credit prices. - Response headers `X-Credits-Used`, `X-Credits-Remaining`, `X-Cost-USD`, `X-Cache`, `X-Latency-Ms`, `X-Tokens-Estimate`, `X-Request-Id`. - Failed, empty and blocked requests are refunded automatically. - Account controls: multiple keys with monthly limits, spend cap, alerts, auto-recharge, teams and a metadata-only request log. ## v0.9 (2026-09): preview The first public preview: JSON search endpoints for Google's verticals, a webpage-to-Markdown fetcher, the CSE-compatible `GET /customsearch/v1`, the three output formats, the batch lane with signed webhooks, the remote MCP server, and the dashboard with keys, usage, billing and spend controls. ## Planned These are on the roadmap but not live. Nothing here is a date commitment. - **OAuth for the MCP server**, so clients that can't send custom headers (ChatGPT connectors, some desktop apps) can sign in directly. ## Incidents This page covers the API contract only. For outages, degraded performance and incident history, see the [status page](https://status.serpkite.com). --- Source: https://serpkite.com/docs/api-keys # API keys > Create as many SerpKite API keys as you need, give each a monthly credit limit, and rotate or revoke them without downtime. Keys are stored as hashes and shown only once. ## Keys at a glance - **Format:** `skt_live_` plus a random secret. The dashboard shows a short prefix such as `skt_live_Ab12` so you can tell keys apart. - **Shown once:** the full secret appears only when the key is created or rotated. SerpKite stores a SHA-256 hash, never the key itself. - **Many per account:** one per service or environment is a good default. - **Account-wide balance:** all keys spend from the same credit balance. A [per-key monthly limit](#per-key-monthly-limit) caps how much of it a single key can use. - **Same rate per key:** each key gets the account's [rate limit](https://serpkite.com/docs/rate-limits) on its own. Manage keys in the dashboard at [app.serpkite.com](https://app.serpkite.com) under **API keys**. On a [team](https://serpkite.com/docs/team), every member can see and manage the account's keys, and each key records who created it. ## Create a key 1. ### Open API keys In the dashboard, open **API keys** and click **Create key**. 2. ### Name it and, optionally, limit it Give the key a name of up to 64 characters that says where it's used, such as `prod-agent` or `n8n`. Optionally set a monthly credit limit. 3. ### Copy the secret Copy the secret into your secrets manager or environment (`SERPKITE_API_KEY`). Once you close the dialog it can't be shown again. ## Per-key monthly limit A key can have a **monthly credit limit**. It counts the credits that key spends in the current UTC calendar month and resets at 00:00 UTC on the first of the month. When a call would push the key past its limit, it is rejected before running with `403 key_limit_reached` and is not billed: ```json { "error": { "code": "key_limit_reached", "message": "this key's monthly credit limit is reached", "request_id": "req_01J8ZKC4T9HV2" } } ``` Other keys keep working. Raise the limit, clear it (no limit) or wait for the next month. The key list shows each key's usage this month next to its limit, and [`GET /v1/account`](https://serpkite.com/docs/endpoints/account) returns the calling key's `credit_limit` and `credits_used_month`, so a service can check its own headroom: cURL: ```bash curl "https://api.serpkite.com/v1/account" \ -H "Authorization: Bearer $SERPKITE_API_KEY" ``` TypeScript: ```ts import { SerpKite } from "serpkite"; const sk = new SerpKite(); const account = await sk.account(); console.log(account.balance, account.month.credits); ``` Python: ```python from serpkite import SerpKite sk = SerpKite() account = sk.account() print(account.balance, account.month.credits) ``` Go: ```go c := serpkite.NewClient() account, err := c.Account(ctx) if err != nil { log.Fatal(err) } fmt.Println(account.Balance, account.Month.Credits) ``` ```json { "balance": 61499.5, "rate_limit_rps": 50, "plan": "paid", "key": { "id": "0192f7a4-…", "name": "prod-agent", "credit_limit": 20000, "credits_used_month": 3120.5 }, "monthly_spend_cap": 50000, "month": { "credits": 4210.5, "requests": 4388 } } ``` Per-key limits are separate from the account-wide [monthly spend cap](https://serpkite.com/docs/spend-controls#monthly-spend-cap). Both apply; whichever is hit first stops the call. Good uses for key limits: - Give each customer, tenant or agent its own key with a budget. - Give a staging or CI key a small limit so a runaway test can't drain the balance. - Hand a contractor or a no-code tool a key with a hard ceiling. ### Limits on a team Only the account owner sets or changes key limits. On a [team](https://serpkite.com/docs/team), the owner can set a **member key limit**: every key a member creates gets that monthly limit automatically, and the member can't raise, lower or remove it. Members can still rename, rotate and revoke the keys they created; the limit stays as it is. Without a member key limit, keys members create have no limit. See [Team](https://serpkite.com/docs/team#member-key-limit). ## Engine fallback Each key has a switch, **Allow fallback to other search engines**, which is off by default. With it on, requests from that key that don't send an `engine` parameter behave as `engine=auto`: when Google is unavailable they may be answered by Brave, Bing and others, and `meta.engine` names the engine that answered. Requests that name an `engine` are unaffected. Toggle it in the key list, or from the key's **Edit** dialog. See [Search providers](https://serpkite.com/docs/providers). ## Rotate a key Rotating replaces the key's secret in place: the old secret stops working and the key keeps its **id, name, monthly limit and this month's usage**, so a key that reached its limit stays at it. The new secret is shown once. To rotate without downtime when a key hasn't leaked, create a second key, deploy it, then revoke the first. Use **Rotate** when you need the old key dead now, for example after it was committed to a repository. ## Revoke a key **Revoke** (delete) makes the key stop working. Requests with it get `401 unauthorized`. Revocation takes effect immediately in most cases and within a minute at most, since validated keys are cached briefly. Revoked keys disappear from the list; their past usage stays in your usage history. ## Key activity For each key the dashboard shows when it was created, who created it, when it was last used (updated at most once a minute), and credits used this month. **Usage** breaks down requests and credits per key, and the request log can be filtered by key. The log stores metadata only, never query text. See [Privacy and data retention](https://serpkite.com/docs/privacy-and-data-retention). ## Managing keys from code The dashboard is backed by an API at `app-api.serpkite.com`, authenticated with the dashboard session cookie rather than an API key. It isn't a public, versioned API yet, but for reference these are the key operations it exposes: | Operation | Request | | --- | --- | | List keys | `GET /v1/keys` | | Create | `POST /v1/keys` with `{"name": "…", "credit_limit": 20000}` | | Rename or change the limit | `PATCH /v1/keys/{id}` (omit `credit_limit` to keep it; `"credit_limit": null` clears it; owner only) | | Turn engine fallback on or off | `PATCH /v1/keys/{id}` with `{"allow_fallback": true}` | | Rotate | `POST /v1/keys/{id}/rotate` | | Revoke | `DELETE /v1/keys/{id}` | ## Related - [Authentication](https://serpkite.com/docs/authentication): How to send a key, and how to keep it secret. - [Spend controls](https://serpkite.com/docs/spend-controls): Account-wide cap, alerts and auto-recharge. - [Team](https://serpkite.com/docs/team): Share keys and balance with teammates. - [GET /v1/account](https://serpkite.com/docs/endpoints/account): Check balance and key limit from code. --- Source: https://serpkite.com/docs/spend-controls # Spend controls > Cap what your account spends per month, get emailed before the balance runs out or the cap is near, and get a one-click checkout link when it's time to top up. ## Overview SerpKite is prepaid, so the most you can ever spend is your balance. Spend controls give you finer limits on top of that. All of them live in the dashboard under **Settings**, and only the account owner can change them. | Control | What it does | Enforced by | | --- | --- | --- | | [Monthly spend cap](#monthly-spend-cap) | Hard limit on credits used per UTC month, across all keys | `403 spend_cap_reached` | | [Per-key limit](https://serpkite.com/docs/api-keys#per-key-monthly-limit) | Hard limit on credits one key uses per UTC month | `403 key_limit_reached` | | [Low-balance alert](#low-balance-alert) | Email when the balance drops below a threshold | Email | | [Usage alert](#usage-alert) | Email when a percentage of the monthly cap is used | Email | | [Auto-recharge](#auto-recharge) | Email a one-click checkout link when the balance drops below a threshold | Email | ## Monthly spend cap The spend cap is the maximum number of credits the whole account may use in a UTC calendar month. It covers every key, every endpoint, the batch lane and [MCP](https://serpkite.com/docs/mcp) calls. It resets at 00:00 UTC on the first of the month. A call that would exceed the cap is rejected before it runs and is not billed: ```json { "error": { "code": "spend_cap_reached", "message": "monthly spend cap reached; raise it at https://app.serpkite.com/settings", "request_id": "req_01J8ZKD2Q7XW4" } } ``` Batch jobs reserve their cost against the cap when you submit them, so a queue of jobs can't overshoot it later. Raise the cap, remove it, or wait for the new month to resume. [`GET /v1/account`](https://serpkite.com/docs/endpoints/account) returns `monthly_spend_cap` and this month's `month.credits`, so you can check headroom from code before a large job. > **Cap or key limit?** > Use the account cap as a safety net for total monthly spend. Use per-key limits to budget individual services, customers or agents. Both apply at the same time. ## Low-balance alert Set a credit threshold and we email the account owner shortly after the balance drops below it. Alerts are checked in the background, and you get one email, not one per request; saving new alert settings re-arms it. Use it to buy the next pack before calls start failing with `402 insufficient_credits`. ## Usage alert Set a percentage between 1 and 100, and we email you when the account has used that share of its monthly spend cap. For example, with a 50,000-credit cap and an 80% alert, you hear from us at 40,000 credits. It fires at most once per month, and only works when a spend cap is set. ## Auto-recharge Auto-recharge keeps production from running dry without storing a card that we charge in the background. 1. ### Pick a threshold and a pack Under **Settings → Auto-recharge**, turn it on, set a balance threshold (for example 5,000 credits) and choose the pack to buy (Starter, Growth or Pro). 2. ### We email a checkout link Shortly after the balance falls below the threshold, we email the account owner a one-click checkout link for that pack, paid through Paddle, our merchant of record. 3. ### You confirm the purchase Open the link and pay. The credits land on your balance as soon as Paddle confirms the payment. Nothing is charged until you complete the checkout, so auto-recharge can never buy credits you didn't approve. If you need fully unattended top-ups, buy a larger pack ahead of time (credits never expire) or talk to us about [Enterprise](https://serpkite.com/enterprise) invoicing. ## Webhook settings **Settings** also holds the account's default **webhook URL** for [batch jobs](https://serpkite.com/docs/batch) and the **webhook signing secret** (`whsec_…`). The secret is shown once when you generate or rotate it; afterwards only its prefix is displayed. See [Webhooks](https://serpkite.com/docs/webhooks) for delivery and signature verification. ## Settings reference The dashboard stores these fields (`GET` and `PUT /v1/settings` on the dashboard API, owner only): | Field | Type | Meaning | | --- | --- | --- | | `monthly_spend_cap` | number or null | Credits per UTC month. `null` means no cap. | | `low_balance_alert` | number or null | Email when the balance drops below this. | | `usage_alert_percent` | integer or null | 1–100. Email when this share of the cap is used. | | `auto_recharge.enabled` | boolean | Turn auto-recharge emails on or off. | | `auto_recharge.threshold` | number | Balance that triggers the email. | | `auto_recharge.pack_id` | string | `starter`, `growth`, `pro`, `business` or `scale`. | | `webhook_url` | string or null | Default webhook for batch jobs. | ## Related - [API keys](https://serpkite.com/docs/api-keys): Per-key monthly credit limits. - [Credits and billing](https://serpkite.com/docs/credits-and-billing): Packs, prices and what's refunded. - [Errors](https://serpkite.com/docs/errors): spend_cap_reached, key_limit_reached and friends. - [Team](https://serpkite.com/docs/team): Who can change settings. --- Source: https://serpkite.com/docs/team # Team > Invite teammates to your SerpKite account so they can manage keys, watch usage and buy credits on one shared balance, while the owner keeps control of settings and the account. ## How teams work A SerpKite account belongs to one **owner**. The owner can invite other people as **members**. Members sign in with their own email, GitHub or Google login, and then act on the owner's account: the same balance, the same API keys, the same usage and billing history. - A user either owns an account or is a member of exactly one other account. - The number of seats (members including the owner) is shown on the **Team** page. The default is 2 seats (the owner and one teammate); [contact us](https://serpkite.com/contact) if you need more. - Credits are shared. There is no per-member balance; use [per-key limits](https://serpkite.com/docs/api-keys#per-key-monthly-limit) and the [member key limit](#member-key-limit) if you need budgets per person or service. ## Roles | Action | Owner | Member | | --- | --- | --- | | Use the account's balance and rate limit | Yes | Yes | | View usage, request log and CSV export | Yes | Yes | | Create API keys; rename, rotate and revoke the ones you created | Yes | Yes | | Set key limits and the member key limit; manage keys other people created | Yes | No | | Buy credit packs, view orders and invoices | Yes | Yes | | Invite and remove members, cancel invites | Yes | No | | Change [spend controls](https://serpkite.com/docs/spend-controls), alerts and webhook settings | Yes | No | | Rotate the webhook signing secret | Yes | No | | Delete the account | Yes | No | | Leave the team | n/a | Yes | Each API key records who created it, so the key list shows which teammate made which key. ## Member key limit The owner can set a default **monthly credit limit for keys that members create** on the **Team** page. Every key a member creates from then on gets that limit, and only the owner can change it afterwards. Members can rename, rotate and revoke their own keys, but can't raise, lower or remove a key's limit, and rotating a key keeps its limit. - By default there is no member key limit, so keys members create have no limit until the owner sets one. - Changing or clearing it applies only to keys created afterwards. Existing keys keep their limit; the owner edits those one by one on the **API keys** page. - Keys the owner creates don't get the member key limit; the owner picks each key's limit. - Members can see the current member key limit on the **Team** page. ## Invite someone 1. ### Send the invite The owner opens **Team** in the dashboard, enters the teammate's email and clicks **Invite**. An email with an accept link goes out. Pending invites are listed with their expiry date and can be cancelled. 2. ### Accept it The teammate opens the link and signs in (or signs up) **with the invited email address**. The invite can only be accepted by a user with that email. 3. ### Start working After accepting, the dashboard shows the owner's account. Balance, keys and usage are the team's. A user can act on only one account. If the invited person already uses SerpKite on their own account, they should use up or move that work first; [contact support](https://serpkite.com/contact) if you need to merge accounts. ## Remove a member or leave - The owner can remove any member from the **Team** page. The member's own login keeps working, but they lose access to the account's keys, usage and billing. - A member can leave the team from the same page. API keys belong to the account, not to the person who created them. Removing a member does **not** revoke the keys they created. If they had access to secrets, [rotate those keys](https://serpkite.com/docs/api-keys#rotate-a-key). ## Deleting the account Only the owner can delete the account. Deleting it permanently removes the account, its keys and its remaining credits, and members lose access. Deletion requires typing the owner's email to confirm. ## Related - [API keys](https://serpkite.com/docs/api-keys): Per-key limits to budget each teammate or service. - [Spend controls](https://serpkite.com/docs/spend-controls): Owner-only caps and alerts. - [Credits and billing](https://serpkite.com/docs/credits-and-billing): One shared balance for the team. - [Privacy and retention](https://serpkite.com/docs/privacy-and-data-retention): What the request log stores. --- Source: https://serpkite.com/docs/privacy-and-data-retention # Privacy and data retention > SerpKite never logs your query text. Results are cached for a few hours under a hashed key, and usage records hold metadata only for 31 days. Here is exactly what is kept, where and for how long. ## The short version - **Query text is never logged.** Not in application logs, not in usage records, not in the request log you see in the dashboard. - **Usage records are metadata only** (endpoint, status, credits, latency, cache hit, which key) and are deleted after **31 days**. - **Results are cached briefly** (up to 6 hours by default, 30 minutes for news, 1 hour for web pages and 24 hours for autocomplete) under a hash of the request, and batch results are kept for 24 hours so you can fetch them. Nothing else is kept. - **API keys, session tokens and sign-in codes are stored as hashes**, never in plain text. This page describes how the service is built. The binding terms are in the [Privacy Policy](https://serpkite.com/legal/privacy) and the [DPA](https://serpkite.com/legal/dpa). ## What we store, and for how long | Data | Contains query text? | Retention | | --- | --- | --- | | Usage events (per request) | No. Endpoint, HTTP status, credits, latency, cache hit, batch flag, error code, API key ID, time | 31 days, then deleted | | Request log and CSV export in the dashboard | No. Built from usage events | 31 days | | Daily usage totals and the credit ledger | No. Credits per day, purchases, grants, refunds | Life of the account (needed for billing) | | Result cache (read with [`max_age`](https://serpkite.com/docs/caching)) | The key is a SHA-256 hash of the normalised request, never the query text. The value is the result, with no account or key attached. Every successful live result is written, whether or not you send `max_age` | In memory only, expires automatically: up to 6 hours by default, 30 minutes for news, 1 hour for web pages and 24 hours for autocomplete | | Batch job request | Yes, until the job runs | Cleared as soon as the job is processed | | Batch job result | Yes (it is the SERP) | 24 hours, then deleted | | Webhook deliveries | Sent to your URL; we keep only the delivery status | With the job | | Application and database logs | No. Never API keys, cookies, `q`, webhook URLs or database query parameters | Short operational retention | | Account data | Email, name, avatar, linked OAuth providers, team membership, settings | Until you delete the account | | Payments | Handled by Paddle, our merchant of record. We store order IDs, pack, amount and credits, not card data | Until you delete the account; Paddle keeps invoices as tax law requires | | Database backups | Account, billing and usage tables (encrypted) | 30 days | Query text is never written to a log or a usage record. The only copies of results are the short-lived ones listed above: cache entries, which deduplicate identical requests and are served only to callers that send `max_age`, and batch results you haven't fetched yet. ## What we don't do - We don't log the `q` parameter, API keys or cookies. - We don't keep results after the retention windows above, and we don't keep a searchable archive of queries. - We don't scrape anything behind a login. SerpKite fetches the public, logged-out Google results page through proxy networks. See [How we collect data](https://serpkite.com/legal/how-we-collect). ## Choosing the privacy trade-off - **Don't want a cached copy served to you?** Don't send `max_age` (or send `0`). Every call is then a live fetch and `X-Cache` is `MISS`. The fresh result is still written to the cache for the windows above; `max_age` only controls reads. - **Don't want batch results held?** Use realtime calls. Batch results always expire after 24 hours, whether you use a [webhook](https://serpkite.com/docs/webhooks) or fetch them. - **Sensitive queries?** Realtime calls leave metadata in usage records (which endpoint was called, when, by which key, and what it cost) and a cache entry under a one-way hash that expires within hours. Raw HTML (`include_html`) is never cached. ## Secrets - **API keys** are stored as SHA-256 hashes. The full key is shown once when created or rotated. See [API keys](https://serpkite.com/docs/api-keys). - **Dashboard sessions** and **sign-in links and codes** are stored as hashes too. You can see and revoke your sessions from the dashboard. - **Webhook signing secrets** are shown once; the dashboard shows only their prefix afterwards. ## Product analytics The website and dashboard use privacy-friendly product analytics (PostHog, EU region) to understand which features are used, **only after you accept analytics cookies** in the cookie banner. Global Privacy Control and Do Not Track count as a refusal. You can change your choice any time under "Cookie settings" in the website footer or Settings → Cookies in the dashboard (see the [Cookie Policy](https://serpkite.com/legal/cookies)). API requests themselves are not sent to analytics. Query strings are stripped from every URL before an event is sent, so playground queries never reach analytics. ## Deleting your data The account owner can delete the account from the dashboard settings. This removes the account, its API keys, remaining credits, ledger, usage events, settings and team, and signs out every session. Copies in encrypted backups roll off within 30 days. Invoices are kept by Paddle, our merchant of record, as tax law requires. For data requests or questions, email privacy@serpkite.com. ## Compliance - Data processing terms: [DPA](https://serpkite.com/legal/dpa). - Who processes data for us: [Subprocessors](https://serpkite.com/legal/subprocessors). - Security practices and how to report a vulnerability: [Security](https://serpkite.com/security). - **SOC 2:** planned, not yet certified. ## Related - [Caching](https://serpkite.com/docs/caching): How max_age and the result cache work. - [Batch requests](https://serpkite.com/docs/batch): Queued jobs and their 24-hour results. - [API keys](https://serpkite.com/docs/api-keys): Hashed storage, rotation and revocation. - [Privacy Policy](https://serpkite.com/legal/privacy): The binding terms.