Quick answer
How do I call a SERP API from Python?
Send a GET request to https://serpcrypto.com/api/v1/search with requests or httpx, your key in the Authorization: Bearer header and the query in params, then read response.json(). SerpCrypto charges 1 credit per successful search, from $0.30/1K searches, and retries of failed calls are free. Pay in crypto, no KYC. The code below needs only requests or httpx.
Python setup at a glance
| Install | pip install requests or pip install httpx |
|---|---|
| Key | Environment variable SERPCRYPTO_API_KEY |
| Header | Authorization: Bearer <key> |
| Timeout | 60 to 90 seconds: a search runs live |
| Retry | 429, 502 and 504 only, with backoff; errors are free |
| Parallel searches | Up to concurrency_limit from GET /credits |
How do I set up a reusable Python client?
Create one requests.Session with the key in its headers and a retry policy for the three temporary errors. A session reuses connections, so a loop of searches spends less time on TLS handshakes. Save the file as serp_client.py; the next examples import it.
Retry from urllib3 waits longer before each new attempt and honours a Retry-After header if one is sent. It never retries 400, 401 or 402, which need a fix, not a second try.
"""serp_client.py: a small, reusable client for the SerpCrypto SERP API (requests)."""
import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
API_URL = "https://serpcrypto.com/api/v1/search"
CREDITS_URL = "https://serpcrypto.com/api/v1/credits"
def make_session():
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["SERPCRYPTO_API_KEY"]
retry = Retry(
total=4,
backoff_factor=1.0, # waits about 1 s, 2 s, 4 s between attempts
status_forcelist=(429, 502, 504),
allowed_methods=("GET",),
respect_retry_after_header=True,
)
session.mount("https://", HTTPAdapter(max_retries=retry))
return session
SESSION = make_session()
def search(q, **params):
"""Run one search (1 credit if it succeeds) and return the parsed JSON."""
r = SESSION.get(API_URL, params={"q": q, **params}, timeout=90)
r.raise_for_status()
return r.json()
def credits():
"""Free: credits left, used today and the concurrency limit."""
r = SESSION.get(CREDITS_URL, timeout=30)
r.raise_for_status()
return r.json()
if __name__ == "__main__":
data = search("project management software", gl="us", hl="en")
for item in data.get("organic_results", []):
print(item["position"], item["link"])
print(credits())How do I collect more than 10 results?
Ask for the next pages one by one. About 10 organic results per page, as Google has served since it stopped honouring num=100 in September 2025; each extra page is one more credit. Stop as soon as pagination.next_page is missing: there is nothing more to fetch, and you keep the credits.
For most rank-tracking work, the first page or the first three pages are enough, because clicks concentrate there. Thirty results for one keyword cost three credits.
from serp_client import search
def top_results(q, pages=3, **params):
"""Collect organic results over several pages: each page is 1 credit."""
rows = []
for page in range(1, pages + 1):
data = search(q, page=page, **params)
for item in data.get("organic_results", []):
rows.append({"page": page, "position": item["position"], "title": item["title"], "link": item["link"]})
if not data.get("pagination", {}).get("next_page"):
break # no further page: stop early and save credits
return rows
rows = top_results("crm for small business", pages=3, gl="gb", hl="en")
print(len(rows), "results")How do I run many searches in parallel?
Use httpx.AsyncClient with an asyncio.Semaphore sized to your account's limit. The script reads concurrency_limit from the free credits endpoint, so it adapts when support raises your limit. Searches above the limit would get 429 anyway; the semaphore avoids wasting those attempts.
Each task retries temporary errors with a growing pause and returns None after four failures, so one stubborn query never blocks the batch.
"""Run many searches in parallel without exceeding the account's concurrency limit."""
import asyncio
import os
import httpx
API_URL = "https://serpcrypto.com/api/v1/search"
CREDITS_URL = "https://serpcrypto.com/api/v1/credits"
HEADERS = {"Authorization": "Bearer " + os.environ["SERPCRYPTO_API_KEY"]}
async def search_one(client, gate, q):
async with gate: # at most N searches at the same time
for attempt in range(4):
r = await client.get(API_URL, params={"q": q, "gl": "us", "hl": "en"})
if r.status_code in (429, 502, 504):
await asyncio.sleep(2 ** attempt)
continue
r.raise_for_status()
return q, r.json()
return q, None
async def main(queries):
async with httpx.AsyncClient(headers=HEADERS, timeout=90) as client:
limits = (await client.get(CREDITS_URL)).json()
gate = asyncio.Semaphore(limits["concurrency_limit"])
results = await asyncio.gather(*(search_one(client, gate, q) for q in queries))
for q, data in results:
first = (data or {}).get("organic_results", [{}])[0].get("link")
print(f"{q!r}: {first}")
asyncio.run(main(["best vpn router", "home espresso machine", "4k monitor for coding"]))How do I save daily rankings to a CSV file?
Append one row per result, with the day, keyword, position, link and the settings the API applied. Reading gl and device from search_parameters rather than from your own variables means the file records what was actually used.
Run the script once a day with cron or the Windows Task Scheduler. Three keywords on mobile cost three credits a day, about 90 a month.
import csv
from datetime import date
from serp_client import search
KEYWORDS = ["standing desk", "ergonomic chair", "monitor arm"]
FIELDS = ["day", "keyword", "position", "title", "link", "gl", "device"]
with open("rankings.csv", "a", newline="", encoding="utf-8") as fh:
writer = csv.DictWriter(fh, fieldnames=FIELDS)
if fh.tell() == 0:
writer.writeheader()
for kw in KEYWORDS:
data = search(kw, gl="gb", hl="en", device="mobile")
applied = data["search_parameters"]
for item in data.get("organic_results", []):
writer.writerow({
"day": date.today().isoformat(),
"keyword": kw,
"position": item["position"],
"title": item["title"],
"link": item["link"],
"gl": applied["gl"],
"device": applied["device"],
})Which Python HTTP client should I use?
Use requests for scripts and cron jobs: it is simple and its retry adapter is mature. Use httpx when you need async code, such as a web app, an agent or a batch of hundreds of keywords. Both send the same request and get the same JSON; the API does not care which one you pick.
Avoid very short timeouts with either library. A client that gives up after five seconds may resend a search that was still running and pay for both.
How much will my script cost?
Count searches, not results: one call is one credit when it succeeds. A job of 500 keywords, one page each, every day for 30 days, is 15,000 searches. On the 25,000 searches pack ($18.75) that leaves room to spare, and Prepaid search credits never expire: unused searches stay on your account until you use them.
Check the balance from code with the credits() helper above; it costs nothing.
Python questions
Is there a SerpCrypto package on PyPI?
No. The examples call the HTTPS endpoint with requests or httpx, two widely used libraries, so there is nothing of ours to install or update. The client above is about 40 lines and you own it.
Which Python versions does this code support?
Python 3.9 and later. The async example uses asyncio.run, available since Python 3.7, and the rest uses only the standard library plus requests or httpx.
Can I use the API from a Jupyter notebook?
Yes. Set SERPCRYPTO_API_KEY before starting Jupyter, or read the key with getpass inside the notebook so it does not end up in a saved cell. In a notebook, use await main(...) instead of asyncio.run(main(...)).
How do I turn results into a pandas DataFrame?
Pass the list of result dictionaries to pandas.DataFrame(data["organic_results"]). Each field becomes a column; missing optional fields such as date become empty cells.