cloro

Migrate to cloro: Field-Level Guides for 6 SERP APIs

Ricardo Batista
Founder, cloro
11 min read
On this page

Disclosure: this is cloro’s own site, so read it as a vendor’s migration guide rather than a neutral review. Every parameter and field name on the other vendors’ side comes from their own live documentation, linked inline. Confirm them there before you write code against them.

What cloro’s request looks like

Whatever you are leaving, the target is the same: a POST with a JSON body, one endpoint per surface, and a bearer token.

POST https://api.cloro.dev/v1/monitor/google
  Authorization: Bearer <key>
  { "query": "serp api",
    "country": "US",
    "location": "Austin, Texas",
    "pages": 1,
    "include": { "aioverview": { "markdown": true } } }

The response splits the page into named arrays rather than one mixed list: result.organicResults[], result.ads[], result.peopleAlsoAsk[], result.relatedSearches[], result.shoppingCards[], result.localResults[], result.knowledgeGraph, and result.aioverview.

That last one is the field most migrations are for. In cloro’s own SERP feature census we scored 65,945 results and found an AI Overview on 87.2% of US results and People Also Ask on 90.6%, so it is not an edge case in the response you are paying for either way.

Migrating from SerpApi

The request

SerpApi puts everything in the query string:

GET https://serpapi.com/search
  ?engine=google
  &q=serp+api
  &location=Austin,+Texas
  &api_key=<key>
SerpApicloroNote
engine=googlethe endpoint pathone endpoint per surface rather than an engine parameter
qquerysame meaning
api_keyAuthorization: Bearer headermove it out of the URL
locationlocation, or uulecloro also accepts a raw UULE string
gl / hlgl / hl, or countrysame names, plus a simpler country shortcut
num / paginationpagesone call returns N pages instead of N calls
include.aioverview.markdownSerpApi returns ai_overview, often as a page_token stub. See below

The response

The organic list is nearly a rename. The array path moves, and one field changes case:

SerpApi fieldcloro field
organic_results[]result.organicResults[]
positionposition
titletitle
linklink
snippetsnippet
displayed_linkdisplayedLink
source(no equivalent)
date
page, which results page the row came from

A parser reading position, title, link and snippet therefore needs one real change: the array path.

The AI Overview costs a second call

SerpApi does return an ai_overview object, so “no AI Overview” would be wrong. What it often returns is a stub carrying only a page_token, because Google serves that block asynchronously. Getting the text and its cited sources means a second, separately billed request to /search?engine=google_ai_overview&page_token=<token>.

We know this because cloro’s monitoring runs SerpApi, DataForSEO and Oxylabs in production against the same prompt set every day, and the SerpApi client carries that follow-up fetch. The same query answers both ways: inline text_blocks and references on one call, a bare token on the next, and which one you get is not predictable from the request. Budget two calls for any query where the AI Overview matters.

cloro returns the block and its sources in the first response when include.aioverview is set, at 2 extra credits.

Working code

# before
r = requests.get(
    "https://serpapi.com/search",
    params={"engine": "google", "q": "serp api", "api_key": KEY},
    timeout=30,
)
for row in r.json()["organic_results"]:
    print(row["position"], row["title"], row["link"])

# after
r = requests.post(
    "https://api.cloro.dev/v1/monitor/google",
    headers={"Authorization": f"Bearer {KEY}"},
    json={"query": "serp api", "country": "US",
          "include": {"aioverview": {"markdown": True}}},
    timeout=30,
)
data = r.json()["result"]
for row in data["organicResults"]:
    print(row["position"], row["title"], row["link"])

overview = data.get("aioverview")   # the part SerpApi leaves you to extract

What depth does to the bill

This is the difference worth modelling before you migrate anything.

SerpApi counts a page of ten results as one search, so a top-100 pull is ten searches. The same query set costs roughly $2 to $4 per 1,000 at ten results and roughly $20 to $40 per 1,000 at a hundred.

cloro is page-driven on one call: a Google Search request is 3 credits, plus 2 for each additional page and 2 for AI Overview enrichment. Ten pages is one call at 3 + 18 credits rather than ten calls, which works out around $1.25 to $2.00 per 1,000 at ten results with the AI Overview included and $5.75 to $9.20 at a hundred.

If your pipeline only ever reads the top ten, the gap is real but modest. If it pulls deep, the billing model is the migration case on its own.

Migrating from DataForSEO

The request

DataForSEO posts an array of task objects, authenticated with HTTP Basic:

POST https://api.dataforseo.com/v3/serp/google/organic/live/advanced
  Authorization: Basic <base64 of login:password>
  [ { "keyword": "serp api",
      "location_name": "United States",
      "language_code": "en",
      "depth": 10 } ]
DataForSEOcloroNote
keywordquerysame meaning
location_name / location_codelocation, country, or uulestrings port directly, no code table
language_codehlsame values
depth (results, max 200)pages (pages of ~10)depth in pages rather than a result count
Basic auth, base64 login:passwordAuthorization: Bearer <key>one credential instead of two
array of tasksone object per callbatch through the async task API instead
include.aioverview.markdownDataForSEO has load_async_ai_overview. See below

The response

Here the migration is mostly deletion. DataForSEO nests organic rows two levels down inside a mixed array; cloro returns them already separated:

DataForSEOcloro
Path to the rowstasks[] → result[] → items[]result.organicResults[]
Filter neededyes, type == "organic"no, the array is already organic
Positionrank_absoluteposition
Titletitletitle
URLurllink
Snippetdescriptionsnippet
Extra fieldsdisplayedLink, date, page

Both the type filter and the two-level unwrap go away.

Two things the docs will not tell you

The AI Overview needs a flag, and the flag costs latency. items[] carries an entry with type: "ai_overview", but without load_async_ai_overview: true on the task it comes back as a stub with items and references null. Setting it adds roughly three seconds to the request. We run DataForSEO in production against a daily prompt set and that flag is in our task payload for exactly this reason.

Success is not an HTTP 200. DataForSEO reports per-task failure inside a 200 response, so a correct client checks status_code == 20000 on the envelope and tasks[0].status_code == 20000 on the task, then reads tasks[0].status_message for the reason. Any error handling you wrote around HTTP status alone will silently treat failures as successes. cloro returns a top-level success boolean and a normal HTTP status.

Working code

# before
r = requests.post(
    "https://api.dataforseo.com/v3/serp/google/organic/live/advanced",
    auth=(LOGIN, PASSWORD),
    json=[{"keyword": "serp api", "location_name": "United States",
           "language_code": "en", "depth": 10}],
    timeout=60,
)
for row in r.json()["tasks"][0]["result"][0]["items"]:
    if row["type"] == "organic":
        print(row["rank_absolute"], row["title"], row["url"])

# after
r = requests.post(
    "https://api.cloro.dev/v1/monitor/google",
    headers={"Authorization": f"Bearer {KEY}"},
    json={"query": "serp api", "country": "US", "hl": "en",
          "include": {"aioverview": {"markdown": True}}},
    timeout=60,
)
for row in r.json()["result"]["organicResults"]:
    print(row["position"], row["title"], row["link"])

The honest cost comparison

DataForSEO is $2.00 per 1,000 in real time. cloro’s Google Search call is 3 credits, 5 with AI Overview enrichment, which lands around $1.25 to $2.00 per 1,000 at ten results on the Hobby plan and lower on higher tiers. So on Google organic the two are level, and cloro carries the AI Overview inside that rate.

So on Google organic alone the two are level on price. The comparison only moves when you count the things you would otherwise build yourself: AI Overview parsing, with the answer and its cited source URLs arriving as a field instead of a scrape of a nested payload; the AI answer engines, since ChatGPT, Perplexity, Copilot, Gemini, Grok and AI Mode run through the same endpoint family and the same credit pool; and the polling loop, which a synchronous endpoint removes.

If your pipeline does none of that, stay on DataForSEO. We would rather you stayed than switched and regretted it.

Migrating from ScraperAPI

The request

ScraperAPI’s structured Google endpoint is a GET with the key in the query string:

GET https://api.scraperapi.com/structured/google/search
  ?api_key=<key>
  &query=serp+api
  &country_code=us
ScraperAPIcloroNote
api_keyAuthorization: Bearer headermove it out of the URL
queryquerysame name, same meaning
country_codecountrysame values
hl / glhl / glunchanged
uuleuuleunchanged
startpagesoffset becomes a page count
output_formatcloro returns JSON only
include.aioverview.markdownnot documented on ScraperAPI’s structured Google endpoint

The response

This is the closest mapping on the page. ScraperAPI returns organic_results[] with position, title, snippet, link and displayed_link, so only the array path and the case of one field change:

ScraperAPIcloro
organic_results[]result.organicResults[]
position, title, snippet, linkidentical
displayed_linkdisplayedLink

The cost

ScraperAPI bills in credits from a monthly pool, and its Google structured endpoint costs about 25 credits a call. On the $49 Hobby plan of 100,000 credits that is roughly 4,000 Google calls, an effective rate near $12 per 1,000. cloro’s 3 credits a call, 5 with AI Overview enrichment, works out to $1.25 to $2.00 per 1,000. On Google specifically this is the largest price gap on this page, and it exists because ScraperAPI is a general proxy scraper carrying the cost of every other target it supports.

Keep ScraperAPI for anything that is not a search engine. It scrapes arbitrary sites, which cloro does not.

Migrating from Oxylabs

The request

Oxylabs Web Scraper API takes a job payload over HTTP Basic auth:

POST https://realtime.oxylabs.io/v1/queries
  Authorization: Basic <base64 of username:password>
  { "source": "google_search",
    "query": "serp api",
    "parse": true }
OxylabscloroNote
source: "google_search"the endpoint pathone endpoint per surface rather than a source field
queryquerysame name
parse: truecloro parses by default, there is no raw mode to opt out of
geo_locationlocation, country, or uulestrings port directly
Basic auth, base64 username:passwordAuthorization: Bearer <key>one credential instead of two
include.aioverview.markdownOxylabs parses ai_overviews (plural). See below

The response

This is the biggest rename on the page. Oxylabs nests organic rows three levels down and abbreviates the field names:

Oxylabscloro
Path to the rowsresults[].content.results.organic[]result.organicResults[]
Positionposposition
Position across the pagepos_overallposition (single sequence)
Titletitletitle
URLurllink
Snippetdescsnippet
Displayed URLurl_showndisplayedLink

Two traps we hit in production

Set locale explicitly or your language follows your geo. Left out, Oxylabs picks the interface language from geo_location: hl=pt for Brazil, hl=ar for the UAE, hl=ga (Irish) for Ireland. SerpApi sends hl=en and DataForSEO language_code=en under the same conditions, so three providers end up answering three different questions while a dashboard presents the answers as one row. We found this comparing the three side by side and now pass locale: "en", which sets hl and leaves gl on the country.

The AI Overview key is plural. Oxylabs ships the block as content.results.ai_overviews, alongside organic, paid and related_questions in the same object. Verified against a live google_search parse on 2026-08-15. A parser looking for ai_overview finds nothing and concludes the block was absent.

The cost

The Oxylabs SERP Scraper API starts at $49/month and bills roughly $2 per 1,000 successful results, which moves retry risk onto the vendor. cloro bills per call at $1.25 to $2.00 per 1,000 at ten results with the AI Overview included. The rates are close enough that price is not the reason to move; the parsed AI Overview and the AI answer engines are.

Geo targeting is worth checking before you commit either way. Oxylabs supports country-level targeting on the developer tier and finer geo above it; cloro accepts any of Google’s canonical geotargets at the same per-call price on every plan.

Oxylabs also sells enterprise proxy infrastructure, a 100M+ IP pool that cloro has no equivalent for. If that is what you are really buying, this is not a migration.

Migrating from Serper

The request

Serper is the smallest API in this set, and the migration is correspondingly small.

POST https://google.serper.dev/search
  X-API-KEY: <key>
  Content-Type: application/json
  { "q": "apple inc" }
SerpercloroNote
X-API-KEY headerAuthorization: Bearer headerboth are headers, so no URL surgery
qquerysame meaning
gl / hlgl / hl, or countrysame names, same values
tbsno date-range filter
pagepagesone call returns N pages
mini-batch, up to 100 queriesthe async task APIbatching moves to a different endpoint
location / uuleSerper has no location parameter in this shape
include.aioverview.markdownSerper returns no AI Overview

The response

The organic rows need no renaming at all. Verified against a live playground call on 2026-09-02:

Serpercloro
organic[]result.organicResults[]
title, link, snippet, positionidentical
peopleAlsoAsk[] with question onlyresult.peopleAlsoAsk[] with question, snippet, markdown and sources[]
relatedSearches[] with queryresult.relatedSearches[] with query and link
credits on the responsecredits on the account, not per response
no equivalentresult.aioverview, result.ads[], result.shoppingCards[], result.knowledgeGraph

So for row in data["organic"] becomes for row in data["result"]["organicResults"] and the loop body is unchanged.

What actually changes

The envelope, not the fields. Serper’s People Also Ask returns the question and nothing else, so you cannot see the answer Google gave or which page it came from. There is no AI Overview block at all.

That is a deliberate design, and it is why Serper is fast and cheap: about $1 per 1,000 queries at entry falling toward $0.30 at volume, against roughly $1.25 to $2.00 for cloro at ten results with the AI Overview included. If the thin envelope is enough for you, Serper is the cheaper tool and you should stay on it. Move when you need to know what the AI Overview said and who it cited.

Migrating from Bright Data

The request

This is the largest structural change on the page, because Bright Data’s Google SERP path is not a SERP API in the usual sense. It is a dataset job:

POST https://api.brightdata.com/datasets/v3/scrape
  Authorization: Bearer <key>
  { "input": [ { "url": "https://www.google.com/search?q=serp+api&gl=us&hl=en&num=10&uule=<encoded location>" } ] }

The response is either the result inline or a snapshot_id. When the initial payload is small, you poll /datasets/v3/snapshot/{id} on an interval until it resolves.

The dataset takes a URL and nothing else. Send it a keyword field and it answers “This input should not contain a keyword field”; leave the URL out and it answers “url: Required field”. So the query, the interface language, the country and the location all have to be encoded into a Google search URL yourself, with the location going in as a uule parameter you generate. We verified that live on 2026-09-01 while wiring Bright Data into cloro’s own provider comparison.

Bright Datacloro
input[].url, with q inside itquery
gl inside the URLcountry or gl
hl inside the URLhl
uule inside the URL, encoded by youlocation as plain text, or uule if you prefer
num inside the URLpages
dataset_idthe endpoint path
snapshot pollinga synchronous response, or the async task API with webhooks

What changes for your code

Three things collapse. The URL builder goes, because parameters become fields. The polling loop goes, because the Google endpoint answers synchronously. And the uule encoder goes, because location accepts a plain string like “Austin, Texas”.

Bright Data remains the right tool for scraping arbitrary sites at scale behind a large residential pool, which cloro does not sell. cloro’s own monitoring runs Bright Data for several AI engine surfaces alongside its own API, so this is a boundary rather than a replacement.

When not to migrate

Be honest about what you actually use. Any of these means you stay where you are, or run both:

  • Engines beyond Google. Bing, Baidu, Yahoo or Yandex as tracked engines, which SerpApi covers and cloro does not.
  • Retail and vertical endpoints. Walmart, Amazon, the App Store, YouTube or Google Maps as first-class products.
  • DataForSEO’s non-SERP products. Keyword data, backlinks, on-page audits, business listings.
  • Arbitrary websites. ScraperAPI, Oxylabs and Bright Data scrape any URL; cloro scrapes search surfaces only.
  • A thin, fast, cheap Google envelope is all you need. Serper is cheaper per query than anything here and its organic rows are the same shape.
  • A residential proxy pool. Oxylabs and Bright Data sell theirs at 100M+ and 72M+ IPs. cloro does not sell proxies at all.
  • More than 100 results in a single call. DataForSEO’s depth goes to 200; cloro pages in tens.
  • A steady monthly volume that fits a SerpApi bundle cleanly, where the bundle discount beats a per-credit pool at your exact volume.

Plenty of teams end up running both, keeping the old vendor for the engine tail and moving the Google and AI-answer slice to a per-credit pool. That is a normal outcome.

The cutover checklist

  1. Take a key on the free tier, 500 credits a month, which is enough for a real diff.
  2. Dual-write for a week: send the same queries to both, and diff the organic lists on domain and position rather than on exact snippet text.
  3. Make the request change. From SerpApi or ScraperAPI, GET becomes POST and api_key moves into the Authorization header. From DataForSEO or Oxylabs, drop the task or job envelope and swap Basic auth for a bearer token.
  4. Make the response change. From SerpApi or ScraperAPI, repoint the array to result.organicResults and rename displayed_link. From DataForSEO, delete the type == "organic" filter and the tasks[] → result[] unwrap, then rename rank_absolute, url and description. From Oxylabs, flatten results[].content.results.organic[] and rename pos, url, desc and url_shown.
  5. Replace pagination with the pages parameter, or the polling loop with a synchronous call. Move to the async task API instead if you want webhooks.
  6. Re-baseline the cost model on credits per call, including the extra 2 credits when AI Overview enrichment is on.
  7. Add result.aioverview and result.peopleAlsoAsk[].sources[] to whatever you report on, which for most teams is the reason they moved.
  8. Cut the old key after a full billing cycle of dual-writes, once you have seen the two bills side by side.

For the pricing picture across every vendor in the category, see the cheapest SERP API breakdown; for the ranked field, the SERP API comparison; and for each vendor against its alternatives, SerpApi, DataForSEO, Serper, Bright Data, ScraperAPI and Oxylabs.

Ricardo Batista

About the author

Founder, cloro

Ricardo is one of the founders and engineers behind its SERP and AI-search scraping infrastructure. Before cloro he scaled a financial comparison site to $7M ARR and ran the full-country operations of a unicorn to $65M ARR, then went back to building. He writes about search engine scraping, generative-engine optimization, and turning live search and AI-answer data into something teams can act on.

Frequently asked questions

How long does it take to migrate a SERP API integration to cloro?

For a single Google Search call, under an hour from either vendor. From SerpApi you are turning a GET with query parameters into a POST with a JSON body and renaming one field. From DataForSEO you are deleting a task envelope and an item-type filter and renaming three. What takes longer is anything downstream that assumed the old vendor's engine catalogue, because cloro covers Google and the AI answer engines rather than Bing, Baidu, Yandex, or the retail verticals.

Is cloro cheaper than the vendor I am on?

Cheaper than SerpApi, and level with DataForSEO. SerpApi runs $2 to $4 per 1,000 at ten results and $20 to $40 at a hundred, because it bills each page of ten as a separate search. DataForSEO is about $2.00 per 1,000. cloro is roughly $1.25 to $2.00 per 1,000 at ten results with the AI Overview included, and about $5.75 to $9.20 at a hundred. All three figures are real-time rates.

What do I gain by moving?

Mostly the AI Overview in the first response. All four vendors can reach that block, so the honest difference is what it costs to get: SerpApi frequently returns a page_token stub and bills a second request for the content, DataForSEO needs load_async_ai_overview on the task and adds about three seconds, and ScraperAPI does not document it on its structured Google endpoint. cloro returns the answer and its cited sources inline for 2 extra credits, and covers ChatGPT, Perplexity, Copilot, Gemini, Grok and AI Mode through the same endpoint family and credit pool. Depth billing is page-driven rather than per-search too.

What do I lose?

Engine breadth from SerpApi, which covers more than 80 engines including Bing, Baidu, Yandex, Walmart, YouTube and the App Store. Non-SERP products from DataForSEO, which sells keyword data, backlinks, on-page and business listings that cloro does not have. If any of those is load-bearing in your pipeline, run both rather than switching.

Can I run both APIs during the migration?

Yes, and you should. Both are stateless HTTPS calls with no shared state, so dual-write the same query set to each for a week and diff the organic lists on domain and position before you cut the old key. The free tier of 500 credits a month covers that diff on a real keyword set.

Which vendors are covered here?

Six: SerpApi, DataForSEO, Serper, Bright Data, ScraperAPI and Oxylabs, each mapped field by field. The target shape is the same whichever you are leaving: POST a JSON body to an endpoint per surface with a bearer token. If you are moving from a vendor not listed, such as Zyte, the cloro half of every table still applies and only your current vendor's parameter names need looking up.

Which migration is the hardest?

Bright Data, because its Google SERP path is a dataset job rather than a search endpoint. It accepts a URL and nothing else, rejecting a keyword field outright, so the query, language, country and location all have to be encoded into a Google search URL with a uule parameter you generate, and the response may arrive as a snapshot id you poll. Moving to a parameterised synchronous endpoint deletes the URL builder, the uule encoder and the polling loop in one go.

Which migration is the easiest?

ScraperAPI, because its structured Google endpoint already returns an `organic_results[]` array whose keys (`position`, `title`, `snippet`, `link`) match cloro's exactly. Only the array path and the auth change. Oxylabs is the most work: its parsed response nests organic rows at `results[].content.results.organic[]` and uses `pos`, `url` and `desc` where cloro uses `position`, `link` and `snippet`.