# SERP API in Python: requests, httpx and asyncio

> Use the SerpCrypto SERP API in Python: a requests client with retries, async httpx with a concurrency limit, pagination and CSV export. Tested code.

- URL: https://serpcrypto.com/docs/python/
- Last updated: 2026-10-22
- Publisher: SerpCrypto (https://serpcrypto.com)

A reusable client with retries, a pagination loop that stops when the results end, async searches capped at your concurrency limit, and a daily CSV for rankings. Copy-paste code, syntax-checked, with no package of ours to install.

**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

| Attribute | Value |
| --- | --- |
| 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*

```python
"""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.

*Pagination: up to three pages, early stop*

```python
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.

*asyncio + httpx: parallel searches under the limit*

```python
"""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.

*Append today's rankings to rankings.csv*

```python
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.

---

SerpCrypto: SerpCrypto is a SERP API for developers and AI agents that returns Google and Bing results as JSON, paid in crypto with no ID upload. Prepaid search credits from $0.30/1K searches never expire. No KYC — no ID, no selfie, no documents. Pay in crypto.
