Introduction
Plainserp is one endpoint that returns Google’s organic results as JSON. You send a query and get back titles, links and snippets. It’s built for AI agents and scripts that need search results and nothing around them.
The base address is https://plainserp.com. Every request is a GET over HTTPS, and every response is JSON.
Quickstart
- Create a key on the API keys page.
- Save it as
PLAINSERP_KEYin your environment. - Make a request.
curl "https://plainserp.com/v1/search?q=postgres+connection+pooling&num=3" \
-H "Authorization: Bearer $PLAINSERP_KEY"{
"query": "postgres connection pooling",
"results": [
{
"position": 1,
"title": "PgBouncer - lightweight connection pooler for PostgreSQL",
"url": "https://www.pgbouncer.org/",
"snippet": "PgBouncer keeps a pool of server connections and hands them to clients…"
},
{
"position": 2,
"title": "PostgreSQL: Documentation: Connections and Authentication",
"url": "https://www.postgresql.org/docs/current/runtime-config-connection.html",
"snippet": "max_connections determines the maximum number of concurrent connections…"
},
{
"position": 3,
"title": "pgpool Wiki",
"url": "https://www.pgpool.net/",
"snippet": "Pgpool-II is a middleware that works between PostgreSQL servers and clients…"
}
],
"cost_usd": 0.0003
}Authentication
Send your key as a Bearer token in the Authorization header. Keys start with ps_live_.
Authorization: Bearer $PLAINSERP_KEYKeep keys on your server. Anyone who has a key can spend your balance, so revoke a key on the dashboard if it leaks.
Search
GET /v1/search runs one Google search and returns the organic results.
| Parameter | Type | Default | Description |
|---|---|---|---|
| q | string | Required | The search query. |
| num | integer | 10 | How many results per page, from 1 to 20. |
| pages | integer | 1 | How many pages to fetch, from 1 to 6. Each page is one search. |
| page | integer | 1 | Which page to start from, from 1 to 6. |
| gl | string | None | Two-letter country code to search from, such as us or de. |
| hl | string | en | Language code for the results, such as en or fr. |
| time | string | None | Only results from the past day, week, month or year. |
| safe | string | off | Safe search level: off, medium or high. |
Response
A successful call returns status 200 with this shape.
| Field | Type | Description |
|---|---|---|
| query | string | The query you sent. |
| results | array | The organic results, in Google's order. |
| results[].position | integer | Rank across all pages, starting at 1. |
| results[].title | string | The page title. |
| results[].url | string | The page address. |
| results[].snippet | string | The text Google shows under the title. |
| results[].thumbnail | string | An image address. Only present when Google has one. |
| cost_usd | number | What the call cost, in US dollars. One search for each page returned. |
Pagination
Google returns results in pages of up to 20, and goes 6 pages deep, which is the first 120 results of a query. Set pages to fetch several pages in one call. They come back as one list, in order.
# Pages 1 to 3: results 1 to 60, charged as three searches
curl "https://plainserp.com/v1/search?q=postgres+connection+pooling&num=20&pages=3" \
-H "Authorization: Bearer $PLAINSERP_KEY"Each page is charged as one search, so the call above costs $0.0009. If a page fails it isn’t charged, and you get the pages that worked. To fetch one page from deeper in the results, set page instead, for example page=4.
Errors
Errors use standard HTTP status codes and a JSON body with a code you can match on. You are never charged for a request that returns an error.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A parameter is missing or out of range. |
| 401 | invalid_api_key | The key is missing, wrong or revoked. |
| 402 | insufficient_balance | Your balance is too low. Top up to continue. |
| 429 | rate_limited | You passed your per-minute limit. Wait for Retry-After, then retry. |
| 502 | upstream_error | Google didn't return results. Retry the request. |
{
"error": {
"code": "insufficient_balance",
"message": "Your balance is too low. Top up to continue."
}
}For 429 and 502, wait a second or two and try again. Doubling the wait after each failed attempt works well.
Rate limits
Each account can make 300 searches a minute by default. The count resets at the start of every clock minute, and a call that fetches several pages counts once per page. Your limit and how much of it you’re using are on the dashboard.
Every response says where you stand. Past the limit you get a 429 with a Retry-After header giving the seconds until the next minute. Requests refused this way are not charged.
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
Retry-After: 23Need more? Message @vvz25 on Telegram and say roughly what you need.
Billing
One successful search costs $0.0003, whatever num is. A call that fetches several pages is one search per page. That is $0.30 for a thousand searches. A new account starts with $0.30, which covers your first 1,000 searches.
You add money in advance from $6, and it doesn’t expire. Every response includes cost_usd, and your balance and daily usage are on the dashboard.
Use with agents
Most agent frameworks take a tool definition and a function to run when the model calls it. This definition works as it is with any model that supports tool use.
{
"name": "web_search",
"description": "Search Google. Returns titles, URLs and snippets.",
"input_schema": {
"type": "object",
"properties": {
"q": { "type": "string", "description": "The search query" },
"num": { "type": "integer", "description": "Results to return, 1 to 20" }
},
"required": ["q"]
}
}import os, requests
def web_search(q: str, num: int = 10) -> list[dict]:
r = requests.get(
"https://plainserp.com/v1/search",
params={"q": q, "num": num},
headers={"Authorization": f"Bearer {os.environ['PLAINSERP_KEY']}"},
timeout=15,
)
r.raise_for_status()
return r.json()["results"]Asking for 5 to 10 results is usually enough for a model to pick what to read, and it keeps the tool result short.
MCP server
To give an AI client search without writing any code, connect it to the Plainserp MCP server. It offers one tool, web_search, and uses your API key and balance like any other call.
| Setting | Value |
|---|---|
| URL | https://plainserp.com/mcp |
| Transport | Streamable HTTP |
| Header | Authorization: Bearer YOUR_KEY |
claude mcp add --transport http plainserp https://plainserp.com/mcp \
--header "Authorization: Bearer $PLAINSERP_KEY"{
"mcpServers": {
"plainserp": {
"type": "http",
"url": "https://plainserp.com/mcp",
"headers": { "Authorization": "Bearer YOUR_KEY" }
}
}
}The exact file and field names vary by client, so check your client’s own instructions for adding a remote server with a header.
Agent skill
A skill is a short instruction file that teaches a coding agent how to use something. The Plainserp skill covers the endpoint, parameters, errors, billing and ready-made code, so the agent can add search to your project correctly the first time.
Open the skill file, or install it for Claude Code:
mkdir -p ~/.claude/skills/plainserp-search
curl -o ~/.claude/skills/plainserp-search/SKILL.md \
https://plainserp.com/skill/plainserp-search/SKILL.mdThen ask your agent to “add web search with Plainserp”. For other agents, put the file wherever that agent reads its instructions from.
What it doesn't do
- It returns organic results only. AI Overviews, People also ask, knowledge panels, ads and image or video packs are left out.
- It doesn’t fetch or read the pages it links to.
- It reaches the first 120 results of a query and no further.
- Plainserp is an independent service and isn’t affiliated with Google. A change on Google’s side can cause a short outage, and failed requests are never charged.
Support
Questions, a payment that didn’t show up, or a higher rate limit: message @vvz25 on Telegram. Include the email on your account.