Respuesta rápida
¿Cómo llamo a una API SERP desde Python?
Envía una petición GET a https://serpcrypto.com/api/v1/search con requests o httpx, tu clave en la cabecera Authorization: Bearer y la consulta en params, y luego lee response.json(). SerpCrypto cobra 1 crédito por búsqueda exitosa, desde 0,30 US$/1K búsquedas, y los reintentos de llamadas fallidas son gratis. Paga con criptomonedas, sin KYC. El código de abajo solo necesita requests o httpx.
Configuración de Python de un vistazo
| Instalación | pip install requests o pip install httpx |
|---|---|
| Clave | Variable de entorno SERPCRYPTO_API_KEY |
| Cabecera | Authorization: Bearer <key> |
| Tiempo de espera | De 60 a 90 segundos: la búsqueda se ejecuta en vivo |
| Reintentos | Solo 429, 502 y 504, con backoff; los errores son gratis |
| Búsquedas en paralelo | Hasta concurrency_limit desde GET /credits |
¿Cómo configuro un cliente de Python reutilizable?
Crea un requests.Session con la clave en sus cabeceras y una política de reintentos para los tres errores temporales. Una sesión reutiliza las conexiones, así que un bucle de búsquedas dedica menos tiempo a los handshakes TLS. Guarda el archivo como serp_client.py; los siguientes ejemplos lo importan.
Retry de urllib3 espera más antes de cada nuevo intento y respeta una cabecera Retry-After si se envía. Nunca reintenta 400, 401 ni 402, que necesitan una corrección, no un segundo intento.
"""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())¿Cómo recopilo más de 10 resultados?
Pide las siguientes páginas una a una. Unos 10 resultados orgánicos por página, tal como Google los sirve desde que dejó de respetar num=100 en septiembre de 2025; cada página adicional es un crédito más. Detente en cuanto falte pagination.next_page: no hay nada más que traer y conservas los créditos.
Para la mayoría de tareas de seguimiento de posiciones, la primera página o las tres primeras son suficientes, porque ahí se concentran los clics. Treinta resultados de una keyword cuestan tres créditos.
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")¿Cómo ejecuto muchas búsquedas en paralelo?
Usa httpx.AsyncClient con un asyncio.Semaphore ajustado al límite de tu cuenta. El script lee concurrency_limit desde el endpoint de créditos gratuitos, así que se adapta cuando soporte sube tu límite. Las búsquedas por encima del límite recibirían 429 de todos modos; el semáforo evita desperdiciar esos intentos.
Cada tarea reintenta los errores temporales con una pausa creciente y devuelve None tras cuatro fallos, así que una consulta problemática nunca bloquea el lote.
"""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"]))¿Cómo guardo los rankings diarios en un archivo CSV?
Añade una fila por resultado, con el día, la keyword, la posición, el enlace y los ajustes que aplicó la API. Leer gl y device desde search_parameters en lugar de tus propias variables hace que el archivo registre lo que se usó realmente.
Ejecuta el script una vez al día con cron o el Programador de tareas de Windows. Tres keywords en móvil cuestan tres créditos al día, unos 90 al mes.
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"],
})¿Qué cliente HTTP de Python debería usar?
Usa requests para scripts y cron jobs: es simple y su adaptador de reintentos está maduro. Usa httpx cuando necesites código asíncrono, como una aplicación web, un agente o un lote de cientos de palabras clave. Ambos envían la misma solicitud y reciben el mismo JSON; a la API le da igual cuál elijas.
Evita timeouts muy cortos con cualquiera de las dos librerías. Un cliente que se rinde a los cinco segundos puede reenviar una búsqueda que seguía en curso y pagar por las dos.
¿Cuánto costará mi script?
Cuenta búsquedas, no resultados: una llamada es un crédito cuando tiene éxito. Un job de 500 palabras clave, una página cada una, todos los días durante 30 días, son 15.000 búsquedas. Con el pack de 25.000 búsquedas (18,75 US$) te sobra margen, y Los créditos de búsqueda prepagados no caducan nunca: las consultas sin usar se quedan en tu cuenta hasta que las uses.
Consulta el saldo desde el código con el helper credits() de arriba; no cuesta nada.
Preguntas sobre Python
¿Hay un paquete de SerpCrypto en PyPI?
No. Los ejemplos llaman al endpoint HTTPS con requests o httpx, dos librerías muy usadas, así que no hay nada nuestro que instalar ni actualizar. El cliente de arriba tiene unas 40 líneas y es tuyo.
¿Qué versiones de Python admite este código?
Python 3.9 y posteriores. El ejemplo asíncrono usa asyncio.run, disponible desde Python 3.7, y el resto solo usa la librería estándar más requests o httpx.
¿Puedo usar la API desde un Jupyter notebook?
Sí. Define SERPCRYPTO_API_KEY antes de iniciar Jupyter, o lee la clave con getpass dentro del notebook para que no acabe en una celda guardada. En un notebook, usa await main(...) en lugar de asyncio.run(main(...)).
¿Cómo convierto los resultados en un DataFrame de pandas?
Pasa la lista de diccionarios de resultados a pandas.DataFrame(data["organic_results"]). Cada campo se convierte en una columna; los campos opcionales que falten, como date, quedan como celdas vacías.