cloro
🐍 Python

The cloro API for Python

One Python client for Google Search, ChatGPT, Perplexity, Gemini, Copilot, and Grok — one authenticated HTTP request per call, structured JSON back. No proxies, no headless browsers, no selectors to maintain.

4.8 · 33 reviewsG2.com software review platform logo

Quickstart

Install a HTTP client, store your API key in the environment, and make your first call. cloro absorbs proxies, rendering, and anti-bot logic on the server — whether you're hitting the Google SERP API or an AI engine like ChatGPT — so your Python code shrinks to an authenticated request and a JSON parse.

pip install requests  # or: pip install httpx

Store your key in the environment — never hardcode secrets:

export CLORO_API_KEY="sk-..."

Authenticate and make the first call:

import os
import requests

API_KEY = os.environ["CLORO_API_KEY"]
BASE_URL = "https://api.cloro.dev/v1"

HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

def search_google(query: str, country: str = "us") -> dict:
    """Single Google SERP call via cloro. Returns parsed JSON."""
    payload = {
        "query": query,
        "country": country,
        "include": ["organic", "ai_overview", "people_also_ask"],
    }
    resp = requests.post(
        f"{BASE_URL}/monitor/google",
        headers=HEADERS,
        json=payload,
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()

print(search_google("best running shoes"))

Every engine, one client

Google Search and every AI engine share the same auth header, request shape, and credit pool. You add coverage by changing the endpoint path, not by writing a new scraper per engine.

EngineEndpointCredits
Google SearchPOST /v1/monitor/google3 +2/page · +2 with AI Overview
Google NewsPOST /v1/monitor/google/news3 +2/page
Google AI ModePOST /v1/monitor/aimode4
ChatGPTPOST /v1/monitor/chatgpt7 (full) · 5 (web search)
PerplexityPOST /v1/monitor/perplexity3
GeminiPOST /v1/monitor/gemini4
CopilotPOST /v1/monitor/copilot5
GrokPOST /v1/monitor/grok4

Base URL: https://api.cloro.dev. Full request and response schemas live in the API docs.

Recipe 1: rank tracker across countries

The most common production job: a nightly tracker that checks a keyword list across several markets and writes results to JSONL you can load into pandas, BigQuery, or a database. It fans out 300 queries (100 keywords × 3 countries) across a capped pool of workers. For the endpoint-level detail, see the Google rank tracking API guide.

import json
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import date

import requests

API_KEY = os.environ["CLORO_API_KEY"]
BASE_URL = "https://api.cloro.dev/v1"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

KEYWORDS = [
    "best running shoes",
    "trail running shoes men",
    "waterproof running shoes",
    # ... up to 100
]
COUNTRIES = ["us", "gb", "de"]
TARGET_DOMAIN = "nike.com"
MAX_WORKERS = 10  # match your plan's concurrency limit, not your CPU count


def fetch_one(query: str, country: str) -> dict:
    payload = {
        "query": query,
        "country": country,
        "include": ["organic", "ai_overview"],
    }
    resp = requests.post(
        f"{BASE_URL}/monitor/google",
        headers=HEADERS,
        json=payload,
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    positions = [
        item["position"]
        for item in data.get("organic", [])
        if TARGET_DOMAIN in item.get("url", "")
    ]
    return {
        "date": str(date.today()),
        "query": query,
        "country": country,
        "rank": positions[0] if positions else None,
        "in_ai_overview": TARGET_DOMAIN
        in str(data.get("ai_overview", {}).get("sources", [])),
    }


def run_tracker(output_path: str = "ranks.jsonl") -> None:
    tasks = [(kw, c) for kw in KEYWORDS for c in COUNTRIES]
    rows: list[dict] = []

    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
        futures = {pool.submit(fetch_one, kw, c): (kw, c) for kw, c in tasks}
        for future in as_completed(futures):
            kw, c = futures[future]
            try:
                rows.append(future.result())
                print(f"OK  {kw} / {c}")
            except Exception as exc:
                print(f"ERR {kw} / {c}: {exc}")

    with open(output_path, "w") as f:
        for row in rows:
            f.write(json.dumps(row) + "\n")

    print(f"\nDone. {len(rows)} rows written to {output_path}")


if __name__ == "__main__":
    run_tracker()

SERP calls are I/O-bound, so threads are the right tool here — not multiprocessing. Cap MAX_WORKERS at your plan's concurrency limit, and keep each returned row flat so it writes cleanly to JSONL, CSV, or a column.

Recipe 2: AI visibility across ChatGPT, Perplexity & Gemini

Rank tracking no longer stops at Google. If your brand appears in a ChatGPT answer but not a Perplexity citation, that gap is worth measuring. Each AI engine is a separate endpoint on the same credit pool — notice how little changes from the rank-tracker code above.

import os
import requests

API_KEY = os.environ["CLORO_API_KEY"]
BASE_URL = "https://api.cloro.dev/v1"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

# Same auth, same request shape — only the endpoint path changes per engine.
AI_ENGINES = {
    "chatgpt":    f"{BASE_URL}/monitor/chatgpt",
    "perplexity": f"{BASE_URL}/monitor/perplexity",
    "gemini":     f"{BASE_URL}/monitor/gemini",
}

BRAND_QUERIES = [
    "best electric SUV 2026",
    "tesla model y vs competitors",
    "ev range comparison",
]
TARGET_BRAND = "tesla.com"


def check_ai_visibility(prompt: str, engine: str, endpoint: str) -> dict:
    payload = {
        "prompt": prompt,
        "country": "us",
        "include": ["answer", "sources", "brand_mentions"],
    }
    resp = requests.post(endpoint, headers=HEADERS, json=payload, timeout=60)
    resp.raise_for_status()
    data = resp.json()
    sources = data.get("sources", [])
    return {
        "engine": engine,
        "prompt": prompt,
        "brand_cited": any(TARGET_BRAND in s for s in sources),
        "answer_snippet": data.get("answer", "")[:200],
    }


def run_ai_visibility_audit() -> list[dict]:
    results = []
    for prompt in BRAND_QUERIES:
        for engine, endpoint in AI_ENGINES.items():
            try:
                row = check_ai_visibility(prompt, engine, endpoint)
                results.append(row)
                status = "cited" if row["brand_cited"] else "not cited"
                print(f"[{engine}] {prompt[:40]}... -> {status}")
            except Exception as exc:
                print(f"ERR [{engine}] {prompt[:40]}...: {exc}")
    return results


if __name__ == "__main__":
    rows = run_ai_visibility_audit()
    cited = sum(1 for r in rows if r["brand_cited"])
    print(f"\n{cited}/{len(rows)} queries cite {TARGET_BRAND} across AI engines")

This is the foundation of AI visibility monitoring: tracking where your brand surfaces (or doesn't) in generative answers alongside traditional search. You add engines by adding endpoints, not by writing new scrapers.

Production concerns

Five things separate a script that works once from one that runs on a schedule:

A minimal async client with backoff and a concurrency cap looks like this:

import asyncio
import os
import random

import httpx

API_KEY = os.environ["CLORO_API_KEY"]
BASE_URL = "https://api.cloro.dev/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}

# Cap concurrency at the plan limit — a Semaphore, not the number of tasks.
SEM = asyncio.Semaphore(10)


async def search(client: httpx.AsyncClient, query: str, max_retries: int = 3) -> dict:
    payload = {"query": query, "country": "us", "include": ["organic"]}
    delay = 1.0
    for attempt in range(max_retries):
        async with SEM:
            resp = await client.post(f"{BASE_URL}/monitor/google", json=payload)
        if resp.status_code == 429 and attempt < max_retries - 1:
            await asyncio.sleep(delay + random.uniform(0, delay * 0.5))
            delay *= 2  # exponential backoff with jitter
            continue
        resp.raise_for_status()
        return resp.json()
    raise RuntimeError("max retries exceeded")


async def main(queries: list[str]) -> list[dict]:
    async with httpx.AsyncClient(headers=HEADERS, timeout=30) as client:
        return await asyncio.gather(*(search(client, q) for q in queries))


if __name__ == "__main__":
    asyncio.run(main(["best running shoes", "trail shoes", "waterproof boots"]))

Error handling

The API returns conventional HTTP status codes. Branch on them once in a wrapper so every call site gets the same behaviour — retry the transient ones, fail fast on the rest.

StatusMeaning
200Success — parsed JSON in the body.
400Bad request — malformed payload or an invalid parameter. Not retryable; fix the request.
401Unauthorized — missing or invalid API key. Check CLORO_API_KEY.
429Rate / concurrency limit hit — back off and retry with jitter.
5xxTransient server error — retry with exponential backoff.
import requests

def call(endpoint: str, payload: dict) -> dict:
    try:
        resp = requests.post(endpoint, headers=HEADERS, json=payload, timeout=30)
        resp.raise_for_status()
        return resp.json()
    except requests.HTTPError as exc:
        status = exc.response.status_code
        if status == 401:
            raise RuntimeError("Invalid or missing CLORO_API_KEY") from exc
        if status == 400:
            raise RuntimeError(f"Bad request: {exc.response.text}") from exc
        if status == 429:
            raise RuntimeError("Rate limit — back off and retry") from exc
        raise  # 5xx: transient, safe to retry with backoff
    except requests.Timeout as exc:
        raise RuntimeError("Request timed out — retry with backoff") from exc

Prefer to build it yourself?

The DIY route — requests + BeautifulSoup + rotating proxies — is real, and sometimes the right call for a one-off. Our Python web scraping pillar guide covers the ecosystem end to end, and How to Scrape Google Search walks through the direct-scrape path, including headless rendering. For anything on a schedule, the API absorbs the maintenance the DIY path never stops demanding.

Pricing that scales with you

Pick a plan that fits your volume. Price per credit drops as you scale.

Hobby
$0.40
per 1,000 credits
  • $100/mo
  • 250,000 credits
  • 20 concurrent jobs
  • Email support
Starter
$0.39
per 1,000 credits
  • $250/mo
  • 650,000 credits
  • 50 concurrent jobs
  • Email support
Most Popular
Growth
$0.37
per 1,000 credits
  • $500/mo
  • 1,350,000 credits
  • 75 concurrent jobs
  • Priority email support
Business
$0.36
per 1,000 credits
  • $1,000/mo
  • 2,800,000 credits
  • 100 concurrent jobs
  • Priority email support
Enterprise
$0.34
per 1,000 credits
/mo
  • 5,871,025 credits
  • 135 concurrent jobs
  • Priority support
Enterprise$5,000+

Increased concurrency, overages on credits and credit discounts for annual contracts.

Know more

Credit cost per request varies by provider. The figures below are for async/batch requests; sync requests add a +2 credit surcharge.

ChatGPT(full response)7 credits
ChatGPT(web search)5 credits
Perplexity3 credits
Grok4 credits
Copilot5 credits
AI Mode4 credits
AI Overview(incl. SERP)5 credits
Gemini4 credits
Google Search3 credits +2/page
Google News3 credits +2/page

ChatGPT full response includes query fan-out, ads, and shopping data. Google News uses the same pricing as Google Search.

Estimate your monthly cost and plan

7 credits each
5 credits each
3 credits each
4 credits each
5 credits each
4 credits each
4 credits each
3 credits / 1 page
3 credits / 1 page
Monthly requests
0
Credits needed
0
Recommended plan:

Comparing providers? See the cheapest SERP APIs breakdown for how cloro's per-call costs stack up across the market.

cloro Python API — frequently asked questions

What is a SERP API for Python?+

A SERP API is a hosted service you call from Python that returns structured JSON for a search query. Your code sends a query with a Bearer token; the API handles proxies, rendering, and anti-bot logic, so you get parsed organic results, AI Overviews, and positions back without maintaining selectors.

How do I call a SERP API from Python?+

Install a HTTP client (pip install requests, or httpx for async), store your key in the CLORO_API_KEY environment variable, then POST your query to the endpoint with an Authorization: Bearer header. The quickstart above is a complete first call you can paste and run.

Should I use requests or httpx to call the API?+

requests is the standard choice for synchronous scripts and rank trackers with modest concurrency. httpx with asyncio is better when you need to fan out hundreds of queries per minute — cap concurrency with an asyncio.Semaphore set to your plan's limit.

Can I query ChatGPT, Perplexity, and Gemini from the same Python client?+

Yes. cloro exposes ChatGPT, Perplexity, Gemini, Copilot, and Grok as separate endpoints on the same credit pool. The auth header and request shape are identical across engines — only the endpoint path changes, so one client covers Google and every AI engine.

How much does the cloro API cost from Python?+

cloro bills a single credit pool: each engine has a fixed per-call credit cost and your plan sets the monthly credit allowance. See the pricing page for current plan rates (it has a calculator to size your volume), and the cheapest SERP API comparison for how per-call costs stack up against other providers.

How do I handle rate limits when calling the API from Python?+

Check for HTTP 429 responses and retry with exponential backoff plus jitter. Cap concurrent workers — a ThreadPoolExecutor for sync code or an asyncio.Semaphore for async — to stay within your plan's concurrency limit rather than flooding the API.

Start building with cloro in Python today

New accounts get 500 free credits — enough to run either recipe, SERP or AI-engine, end to end before you pick a plan.