serpcheap
Official Ruby client for the serp.cheap Google SERP API.
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. # 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