Project

serpcheap

0.0
The project is in a healthy, maintained state
A thin, dependency-free client for the serp.cheap SERP API: search, scrape, and rank, built on net/http.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

serpcheap

Gem Version Gem downloads License: MIT

Official Ruby client for the serp.cheap Google Search API — real-time Google SERP data (organic results, ads, knowledge graph, page scraping, rank tracking).

The cheapest Google Search API we know of: $0.0003 per cached search, $0.0006 fresh, no monthly minimum (~10× cheaper than SerpApi).

A thin, dependency-free client built on net/http. Works on Ruby 2.7+.

Install

gem install serpcheap

Or in a Gemfile:

gem "serpcheap"

Quickstart

require "serpcheap"

client = SerpCheap::Client.new("KEY")
res = client.search("best running shoes", gl: "us")

puts res.organic.first.title

Get an API key at app.serp.cheap.

Search parameters

client.search("best running shoes",
  gl: "us",      # country, default "us"
  hl: "en",      # UI language (optional)
  tbs: "qdr:d",  # time filter: qdr:h / qdr:d / qdr:w (optional)
  page: 1        # 1-indexed page, default 1
)

The response is a SerpCheap::SearchResponse with reader methods:

res.search           # the query (String)
res.page             # page number (Integer)
res.organic          # Array<OrganicResult> (always an array)
res.ads              # Array<Ad> or nil
res.knowledge_graph  # KnowledgeGraph or nil
res.people_also_ask  # Array<String> or nil
res.related_searches # Array<RelatedSearch> or nil
res.stats            # SearchStats(balance, cost, cached) or nil

Multiple pages

# Eagerly fetch pages 1..5; stops on the first empty page.
pages = client.search_pages("best running shoes", from: 1, to: 5, gl: "us")

Scrape page content with the search

Attach page scraping to a search — each organic result gains content (markdown) and, when requested, a screenshot_url (48h presigned URL):

res = client.search("best running shoes", scrape: {
  render_js: true,   # headless render for JS-heavy pages
  screenshot: true,  # capture a full-page screenshot
  top_n: 3           # how many top results to scrape (default 5)
})

res.organic.first.content        # markdown, or nil
res.organic.first.screenshot_url # String, or nil
res.organic.first.scrape_error   # why a page couldn't be scraped, or nil

Scrape a single page

page = client.scrape("https://example.com",
  render_js: true,
  screenshot: true,
  wait_for: "#main",      # CSS selector to await (render_js only)
  wait_ms: 500,           # extra settle time (render_js only)
  screenshot_width: 1920, # default 1920, max 1920
  screenshot_height: 1080 # default 1080, max 1920
)

page.title          # String or nil
page.content        # markdown or nil
page.content_text   # plain text or nil
page.screenshot_url # String or nil
page.stats          # ScrapeStats(balance, cost) or nil

Rank tracking

Find where a domain or URL ranks for a keyword:

res = client.rank("example.com", "best running shoes",
  gl: "us",
  pages: 3,            # result pages to scan, 1..10 (default 1)
  match_type: "domain" # "domain" (registrable domain) or "exact" (identical URL)
)

res.found    # boolean
res.rank     # absolute rank of the best match, or nil
res.matches  # Array<RankMatch> (rank, page, position_on_page, link, title)
res.organic  # Array<OrganicResult> across scanned pages
res.stats    # RankStats(balance, cost, pages_cached, pages_fresh) or nil

Client options

SerpCheap::Client.new("KEY",
  base_url: "https://api.serp.cheap", # default
  timeout_ms: 15_000,                 # default
  max_retries: 2                      # default
)

Transient failures (429, 503, timeouts, network errors) are retried with backoff, honoring the API's retry_after_ms. 4xx errors are never retried.

Errors

Every failure raises SerpCheap::Error:

begin
  client.search("coffee")
rescue SerpCheap::Error => e
  e.error_code     # e.g. "insufficient_credits", "rate_limited"
  e.status         # HTTP status (Integer or nil)
  e.retry_after_ms # set for rate_limited (Integer or nil)
  e.retryable?     # boolean
end

Error codes mirror the API taxonomy: invalid_request, missing_api_key, unknown_api_key, inactive_api_key, account_blocked, insufficient_credits, rate_limited, request_in_progress, too_many_concurrent_requests, service_temporarily_unavailable, result_timeout, plus the client-side client_timeout, network_error, and invalid_response.

License

MIT