DEVELOPER DOCUMENTATION

Serper.live API documentation

One endpoint that runs a real Google search for the market you choose and returns the organic results as JSON. Every request is live: nothing is cached, nothing is replayed from an archive.

Base URL
https://api.serper.live
Authentication
Bearer token
Format
JSON over HTTPS
On this page
01

Quick start

Three steps from an empty account to your first live result.

  1. Create an account and a token in API tokens. The full token is shown once, so store it in your secret manager.
  2. Your first 100 searches are free. Add credit by card or in USDT or USDC on Balance when you need more; the price and a cost calculator are on the pricing page.
  3. Send the request below with your token. The response is the current Google result set for that market.
curl
curl -X POST https://api.serper.live/v1/search \
  -H "Authorization: Bearer sl_live_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"best coffee shops","country":"AE"}'
JavaScript
const response = await fetch('https://api.serper.live/v1/search', {
    method: 'POST',
    headers: {
        Authorization: 'Bearer sl_live_YOUR_TOKEN',
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query: 'best coffee shops', country: 'AE' }),
    signal: AbortSignal.timeout(100000),
});
const result = await response.json();

console.log(result.organic);
Python
import requests

response = requests.post(
    "https://api.serper.live/v1/search",
    headers={"Authorization": "Bearer sl_live_YOUR_TOKEN"},
    json={"query": "best coffee shops", "country": "AE"},
    timeout=100,
)
print(response.json()["organic"])
PHP
<?php

$context = stream_context_create(['http' => [
    'method' => 'POST',
    'header' => "Authorization: Bearer sl_live_YOUR_TOKEN\r\nContent-Type: application/json",
    'content' => json_encode(['query' => 'best coffee shops', 'country' => 'AE']),
    'timeout' => 100,
]]);
$result = json_decode(file_get_contents('https://api.serper.live/v1/search', false, $context), true);

print_r($result['organic']);

A live search usually returns in 2–3 seconds; the first request to a market after a pause takes longer while a fresh Google session opens there. Keep a client timeout of at least 100 seconds so a slow first request never fails on your side.

02

Authentication

Every request to /v1/search carries your token in the Authorization header:

HTTP
Authorization: Bearer sl_live_YOUR_TOKEN
  • Tokens start with sl_live_ and are created in API tokens. Create one token per application so you can revoke it without touching the others.
  • Keep tokens on your server. Never ship them in a browser, a mobile app or a public repository.
  • A revoked token stops working immediately; the history of its requests stays in your account.
HTTP Code Meaning
401 unauthorized The Authorization header is missing.
401 invalid_token The token is malformed, revoked or unknown.
03

POST https://api.serper.live/v1/search

Runs one live Google search and returns the organic results. Request and response are JSON; the request body is limited to 16 KB.

Request body

Field Type Required Description
query string yes 2–120 characters. Whitespace is collapsed; control characters are rejected.
country string yes Two-letter ISO code of the market, for example AE. Must be one of the codes from GET /v1/countries.
language string no Google interface language (hl), for example tr, en or pt-BR. Defaults to the language of the country.
device string no desktop (default) or mobile. Changes the browser persona and therefore the result layout.
limit integer no Number of organic results, 1–100. Default 10. Every started block of 10 reserves one price: limit 25 reserves three, and only blocks that received results are charged.

Response

Field Description
requestId UUID of this request. It appears in your request history; quote it when you report a problem.
query, country, language, device The normalised values that were actually used.
fetchedAt ISO 8601 timestamp of the live fetch.
latencyMs How long the live search took, in milliseconds.
pages Blocks of 10 results that received results: never more than you asked for, 0 when Google found nothing.
freePages How many of those blocks were covered by the free searches of your account: they are used before your balance.
price, currency What this request cost, as a number in USD: the market price multiplied by the paid blocks (pages minus freePages). 0 when nothing was found or the free searches covered the request.
organic[] Organic results in ranking order: position, title, url, domain and snippet. snippet is null when Google shows no description.
peopleAlsoAsk[] Questions from Google's "People also ask" block, as plain strings in page order; an empty array when Google shows none.
relatedSearches[] Related searches Google suggests under the results ("People also search for"), as plain strings without duplicates; an empty array when there are none.
200 OK
{
    "requestId": "0199860b-2f4e-7a1c-9b3d-5e6f70819a2b",
    "query": "best coffee shops",
    "country": "AE",
    "language": "en",
    "device": "desktop",
    "fetchedAt": "2026-09-28T14:00:00.000Z",
    "latencyMs": 1873,
    "pages": 1,
    "freePages": 0,
    "price": 0.005,
    "currency": "USD",
    "organic": [
        {
            "position": 1,
            "title": "The 12 best coffee shops in town",
            "url": "https://example.com/guides/best-coffee-shops",
            "domain": "example.com",
            "snippet": "A curated list of specialty coffee shops, updated every month."
        },
        {
            "position": 2,
            "title": "Coffee shops near me",
            "url": "https://maps.example.org/coffee",
            "domain": "maps.example.org",
            "snippet": null
        }
    ],
    "peopleAlsoAsk": [
        "What is the best coffee shop in town?",
        "How much does a specialty coffee cost?"
    ],
    "relatedSearches": [
        "best coffee shops near me",
        "specialty coffee roasters",
        "coffee shop with wifi"
    ]
}
Results are collected across result pages until limit is reached, without Google's own navigation entries and without duplicates.

Organic results: the organic[] fields

Each element of organic[] is one organic result, in the order Google shows them. Google's own navigation entries and duplicates are left out, and results are collected across result pages until limit is reached.

The fields are the same for desktop and mobile and for every market:

Field Type Description
position integer The rank among organic results, from 1 up to limit (at most 100), counted continuously across Google's result pages. It counts organic entries only, so it is the number a rank tracker stores.
title string The result title as shown on the page.
url string The full address of the result.
domain string The host part of url, for example maps.example.org. Match this field when you look for a site.
snippet string or null The description shown under the title; null when Google shows none.
04

Countries

GET https://api.serper.live/v1/countries

Public, no token needed. Returns the markets that are open right now and the default Google language of each; every market supports both desktop and mobile.

curl
curl https://api.serper.live/v1/countries
JavaScript
const response = await fetch('https://api.serper.live/v1/countries');
const { countries } = await response.json();

console.log(countries);
Python
import requests

response = requests.get("https://api.serper.live/v1/countries", timeout=10)
print(response.json()["countries"])
PHP
<?php

$result = json_decode(file_get_contents('https://api.serper.live/v1/countries'), true);

print_r($result['countries']);
200 OK
{
    "countries": [
        {
            "code": "AE",
            "defaultLanguage": "ar"
        },
        {
            "code": "AR",
            "defaultLanguage": "es"
        },
        {
            "code": "AT",
            "defaultLanguage": "de"
        },
        {
            "code": "AU",
            "defaultLanguage": "en"
        },
        {
            "code": "AZ",
            "defaultLanguage": "az"
        },
        {
            "code": "BE",
            "defaultLanguage": "nl"
        },
        {
            "code": "BG",
            "defaultLanguage": "bg"
        },
        {
            "code": "BR",
            "defaultLanguage": "pt"
        },
        {
            "code": "CA",
            "defaultLanguage": "en"
        },
        {
            "code": "CH",
            "defaultLanguage": "de"
        },
        {
            "code": "CL",
            "defaultLanguage": "es"
        },
        {
            "code": "CO",
            "defaultLanguage": "es"
        },
        {
            "code": "CZ",
            "defaultLanguage": "cs"
        },
        {
            "code": "DE",
            "defaultLanguage": "de"
        },
        {
            "code": "DK",
            "defaultLanguage": "da"
        },
        {
            "code": "EG",
            "defaultLanguage": "ar"
        },
        {
            "code": "ES",
            "defaultLanguage": "es"
        },
        {
            "code": "FI",
            "defaultLanguage": "fi"
        },
        {
            "code": "FR",
            "defaultLanguage": "fr"
        },
        {
            "code": "GB",
            "defaultLanguage": "en"
        },
        {
            "code": "GR",
            "defaultLanguage": "el"
        },
        {
            "code": "HU",
            "defaultLanguage": "hu"
        },
        {
            "code": "ID",
            "defaultLanguage": "id"
        },
        {
            "code": "IE",
            "defaultLanguage": "en"
        },
        {
            "code": "IN",
            "defaultLanguage": "hi"
        },
        {
            "code": "IT",
            "defaultLanguage": "it"
        },
        {
            "code": "JP",
            "defaultLanguage": "ja"
        },
        {
            "code": "KR",
            "defaultLanguage": "ko"
        },
        {
            "code": "KZ",
            "defaultLanguage": "ru"
        },
        {
            "code": "MX",
            "defaultLanguage": "es"
        },
        {
            "code": "NL",
            "defaultLanguage": "nl"
        },
        {
            "code": "NO",
            "defaultLanguage": "nb"
        },
        {
            "code": "PE",
            "defaultLanguage": "es"
        },
        {
            "code": "PL",
            "defaultLanguage": "pl"
        },
        {
            "code": "PT",
            "defaultLanguage": "pt"
        },
        {
            "code": "RO",
            "defaultLanguage": "ro"
        },
        {
            "code": "SA",
            "defaultLanguage": "ar"
        },
        {
            "code": "SE",
            "defaultLanguage": "sv"
        },
        {
            "code": "TH",
            "defaultLanguage": "th"
        },
        {
            "code": "TR",
            "defaultLanguage": "tr"
        },
        {
            "code": "UA",
            "defaultLanguage": "uk"
        },
        {
            "code": "US",
            "defaultLanguage": "en"
        },
        {
            "code": "VN",
            "defaultLanguage": "vi"
        },
        {
            "code": "ZA",
            "defaultLanguage": "en"
        }
    ]
}

New markets appear in this response and on the pricing page as they launch. A request for a closed market returns 422 country_unavailable together with the list of available codes.

05

Billing

Your account holds a prepaid balance in USD and starts with 100 free searches. Before the search starts, each request reserves the current price of its market for every started block of 10 results: limit 10 or less reserves one price, limit 25 reserves three. Free searches cover the first blocks of a request before any money is reserved.

  • Successful search: the price is charged for every block of 10 that received results, and the rest of the reservation returns to your balance at once. limit 50 with 12 results costs two prices, not five; a search that finds nothing costs nothing.
  • Free searches: every new account starts with 100 of them, one per block of 10 results, on any market and device. A request uses them before your balance, and blocks that received no results go back to the free pool the same way money goes back to the balance; freePages in the response says how many blocks a request took from them.
  • Failed search, that is a 408, 424 or 5xx response: the reservation is released automatically. You are never charged for a result you did not receive.
  • Prices can change over time, but the price reserved for a running request never does.
  • Not enough credit: 402 insufficient_balance. Top up on Balance; the credit is available within minutes of the deposit being confirmed.

Every request, its status and its cost are listed in Request history; every balance change is in the ledger on the balance page.

06

Errors

Errors are JSON with a machine-readable error code, a human-readable message and, once a request has been created, its requestId:

JSON
{
    "error": "insufficient_balance",
    "message": "The balance does not cover one request",
    "requestId": "0199860b-2f4e-7a1c-9b3d-5e6f70819a2b"
}
HTTP Code Meaning Retry?
400 invalid_json The body is not valid JSON. Fix the request
401 unauthorized
invalid_token
See Authentication. Fix the token
402 insufficient_balance The balance does not cover one request. After a top-up
408 search_timeout The search did not finish within 90 seconds. Yes
409 idempotency_in_progress
idempotency_replay_unavailable
See Safe retries. Depends on the code
413 payload_too_large The body exceeds 16 KB. Fix the request
422 invalid_request
country_unavailable
A field failed validation (the message names it), or the market is not open. Fix the request
424 proxy_unavailable
captcha_failed
search_failed
The live search failed on our side. Yes, after a short pause
503 busy
billing_unavailable
configuration_error
The service is at capacity or temporarily unavailable. Yes, with backoff
No 408, 424 or 5xx response is ever charged. Retry 408, 424 and 503 with exponential backoff, for example after 5, 15 and 45 seconds, rather than in a tight loop, and with a new Idempotency-Key: the same key replays the same error for up to 15 minutes instead of starting a new search.
07

Safe retries

Optional. You only need this if your client may retry a request that was cut off.

A live search runs for up to 90 seconds. If your connection drops in the meantime, you cannot tell whether the search finished and was charged. Sending an Idempotency-Key header lets you retry without paying twice:

  • Same key: if the first search finished, you get the same response again with the header Idempotent-Replayed: true and no second charge; a search that failed replays the same error. If it is still running, the retry waits for it. Reuse the key only when you did not receive the outcome, after a dropped connection or a client timeout; after a 408, 424 or 5xx send a new key.
  • The key alone identifies the search: a retry with a different body gets the first request’s response, so send a new search with a new key.
  • Results are kept for up to 15 minutes. After that a retry with the old key returns 409 idempotency_replay_unavailable; send a new request with a new key.
  • Without the header every request is new and billed separately.
curl
curl -X POST https://api.serper.live/v1/search \
  -H "Authorization: Bearer sl_live_YOUR_TOKEN" \
  -H "Idempotency-Key: 0199860b-2f4e-7a1c-9b3d-5e6f70819a2b" \
  -H "Content-Type: application/json" \
  -d '{"query":"best coffee shops","country":"AE","limit":10}'
JavaScript
const response = await fetch('https://api.serper.live/v1/search', {
    method: 'POST',
    headers: {
        Authorization: 'Bearer sl_live_YOUR_TOKEN',
        'Idempotency-Key': '0199860b-2f4e-7a1c-9b3d-5e6f70819a2b',
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query: 'best coffee shops', country: 'AE', limit: 10 }),
    signal: AbortSignal.timeout(100000),
});
const result = await response.json();
Python
import requests

response = requests.post(
    "https://api.serper.live/v1/search",
    headers={
        "Authorization": "Bearer sl_live_YOUR_TOKEN",
        "Idempotency-Key": "0199860b-2f4e-7a1c-9b3d-5e6f70819a2b",
    },
    json={"query": "best coffee shops", "country": "AE", "limit": 10},
    timeout=100,
)
result = response.json()
PHP
<?php

$context = stream_context_create(['http' => [
    'method' => 'POST',
    'header' => "Authorization: Bearer sl_live_YOUR_TOKEN\r\nIdempotency-Key: 0199860b-2f4e-7a1c-9b3d-5e6f70819a2b\r\nContent-Type: application/json",
    'content' => json_encode(['query' => 'best coffee shops', 'country' => 'AE', 'limit' => 10]),
    'timeout' => 100,
]]);
$result = json_decode(file_get_contents('https://api.serper.live/v1/search', false, $context), true);

The key is any string of 8–128 characters from A-Za-z0-9._:-. A fresh UUID per logical request is the simplest choice; keep it identical across the retries of that request.

08

Limits and good to know

  • query 2–120 characters, limit 1–100, request body up to 16 KB.
  • Searches run concurrently up to the service capacity; beyond it you get 503 busy. Retry with backoff.
  • Usually 2–3 seconds for 10 results and about a second more per further page; a search still running after 90 seconds ends with 408 search_timeout. Set your client timeout to at least 100 seconds.
  • Every response carries an X-Request-Id header. Keep it in your logs next to the requestId from the body.
  • Nothing is cached: two identical requests are two live searches and two charges. Cache on your side if you reuse results.
09

Finding a domain's position

The API returns the organic results up to limit, the top 10 by default; finding one site in it is a loop over organic[] in your code. Compare the domain of each entry with the host you track, and the position of the first match is the rank for that keyword, country and device. The example below does exactly that, with the same request in curl, JavaScript and Python.

Three rules keep the comparison honest:

  • Normalise before comparing: lower-case both sides, strip a leading www. and compare hosts only, never full URLs.
  • Decide how subdomains count. blog.example.com is a different host from example.com; match the exact host for a strict tracker, or treat any host ending in .example.com as the same site for a brand-level one.
  • No match means outside the requested range for this check, not an error. Store it as such: a request returns at most limit organic results, so raise limit (up to 100, one price per 10) if you track positions beyond the top 10.
curl
curl -s -X POST https://api.serper.live/v1/search \
  -H "Authorization: Bearer sl_live_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"best coffee shops","country":"AE","device":"desktop"}' \
  | jq '.organic[] | select(.domain == "example.com") | .position'
JavaScript
const response = await fetch('https://api.serper.live/v1/search', {
    method: 'POST',
    headers: {
        Authorization: 'Bearer sl_live_YOUR_TOKEN',
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query: 'best coffee shops', country: 'AE', device: 'desktop' }),
    signal: AbortSignal.timeout(100000),
});
const { organic } = await response.json();
const hit = organic.find(row => row.domain === 'example.com');

console.log(hit ? hit.position : 'outside the top 10');
Python
import requests

response = requests.post(
    "https://api.serper.live/v1/search",
    headers={"Authorization": "Bearer sl_live_YOUR_TOKEN"},
    json={"query": "best coffee shops", "country": "AE", "device": "desktop"},
    timeout=100,
)
organic = response.json()["organic"]
position = next((row["position"] for row in organic if row["domain"] == "example.com"), None)
print(position or "outside the top 10")
PHP
<?php

$context = stream_context_create(['http' => [
    'method' => 'POST',
    'header' => "Authorization: Bearer sl_live_YOUR_TOKEN\r\nContent-Type: application/json",
    'content' => json_encode(['query' => 'best coffee shops', 'country' => 'AE', 'device' => 'desktop']),
    'timeout' => 100,
]]);
$result = json_decode(file_get_contents('https://api.serper.live/v1/search', false, $context), true);
$hit = array_find($result['organic'], fn(array $row) => $row['domain'] === 'example.com');

echo $hit['position'] ?? 'outside the top 10';

The rank tracking API page covers the full cycle of a tracker: keyword lists, scheduling, storage and what a month of checks costs.

10

Use with AI agents (MCP)

The Serper.live MCP server gives AI agents such as Claude, OpenAI Codex, Cursor and VS Code Copilot live Google search through this API. It runs locally with npx, needs Node.js 20 or newer and authenticates with the same API token in the SERPER_LIVE_TOKEN environment variable; searches are billed exactly like POST /v1/search.

  • google_search: live organic results for a keyword, country, language and device, up to 100 per call.
  • domain_position: where a domain ranks for a keyword; a subdomain counts as the domain, and every position is returned if the site appears more than once.
  • list_countries: the open markets and the default Google language of each; free, no token.
Claude Code
claude mcp add serper-live -e SERPER_LIVE_TOKEN=sl_live_YOUR_TOKEN -- npx -y @serper-live/mcp
Claude Desktop · Cursor
{
    "mcpServers": {
        "serper-live": {
            "command": "npx",
            "args": [
                "-y",
                "@serper-live/mcp"
            ],
            "env": {
                "SERPER_LIVE_TOKEN": "sl_live_YOUR_TOKEN"
            }
        }
    }
}
VS Code
{
    "inputs": [
        {
            "type": "promptString",
            "id": "serper-live-token",
            "description": "Serper.live API token",
            "password": true
        }
    ],
    "servers": {
        "serper-live": {
            "type": "stdio",
            "command": "npx",
            "args": [
                "-y",
                "@serper-live/mcp"
            ],
            "env": {
                "SERPER_LIVE_TOKEN": "${input:serper-live-token}"
            }
        }
    }
}
Codex
[mcp_servers.serper-live]
command = "npx"
args = ["-y", "@serper-live/mcp"]

[mcp_servers.serper-live.env]
SERPER_LIVE_TOKEN = "sl_live_YOUR_TOKEN"

The package is @serper-live/mcp on npm and io.github.serper-live/serper-live-mcp in the official MCP Registry; the README in the repository covers setup for more clients. Claude Desktop also installs it in one click from the .mcpb bundle and asks for the token itself, without Node.js.

Get started

Send your first live request

Create an account, get 100 free searches and send the request from the quick start; the first live result is seconds away. Add credit by card from $10 or in USDT or USDC from $5 when you need more. Not ready for a token yet? The playground runs three live searches per IP address per day without an account and shows the same organic results.