serpapi-cli
Search engine results as JSON from SerpApi, for people, scripts and AI agents
Installation
Homebrew (macOS/Linux)
brew tap serpapi/homebrew-tap
brew install serpapi-cli
Pre-built binaries
Download directly from GitHub Releases
Go (compile from source)
go install github.com/serpapi/serpapi-cli/cmd/serpapi@latest
This needs Go 1.25 or newer; with the default GOTOOLCHAIN=auto, Go 1.21 and later download it on their own.
Quick start
Your API key is at https://serpapi.com/manage-api-key. Without one, search still answers a few demo queries such as q=coffee.
serpapi login # check and save your API key
serpapi search engine=google q=coffee # JSON results
serpapi search engine=google coffee --fields "organic_results[].{title, link}"
serpapi search engine=google q=coffee output=md # Markdown
serpapi --help and each command's --help document parameters, errors and costs.
Commands
search
Parameters are key=value pairs sent to the API as-is; words without = are joined into q, and a parameter given twice keeps its last value, with a warning. Each engine's parameters are listed at https://serpapi.com/search-engine-apis. Some engines name the query differently: search_query (YouTube), k (Amazon), _nkw (eBay), query (Walmart, Naver), p (Yahoo), text (Yandex).
serpapi search engine=google q="coffee shops" location="Austin, Texas, United States"
serpapi search engine=youtube search_query="lofi beats" --jq '.video_results[:3][].link'
serpapi search engine=google_maps q=pizza ll="@40.7455096,-74.0083012,14z" --fields "local_results[0:5]"
--fields sets SerpApi's json_restrictor, so the server returns only the listed paths:
--fields |
Returns |
|---|---|
organic_results[0:3] |
the first three results |
organic_results[].{title, link} |
two fields of every result |
search_metadata.id,local_results[0] |
several paths, comma-separated |
Unknown paths are dropped silently; if nothing matches, you get {} and a warning on stderr. The server also drops null, false and empty values. --fields is part of the cache key, so a different --fields can miss the cache and count as a new search; --jq filters the response locally without changing the request.
output=md returns Markdown; html, plain and csv are also available. They are not JSON, so --jq and --all-pages don't apply.
--all-pages follows serpapi_pagination.next and merges the arrays of every page into one object, up to --max-pages pages (default 100). Each page is one search. It stops at a page that --fields matches nothing on, so a typo costs one search; to page past such pages, filter with --jq instead. If a later page fails, or Ctrl-C stops the run, the pages fetched so far are printed and the error says which page it stopped at.
serpapi search engine=google q=coffee --all-pages --max-pages 3 --jq '.organic_results | length'
async=true returns at once with search_metadata.id; collect the results with serpapi archive.
Image Search (Google Lens)
-
--image <file>— Upload an image with the Image API and search with itsimage_idin one step. Use-to read the image from stdin. Onlyengine=google_lenstakes animage_id: other engines drop it and would run a normal search without the image, so they are refused before the upload, and so isurl=.
# Reverse image search with Google Lens
serpapi search engine=google_lens --image ./photo.jpg
# Pipe an image in from another command
curl -s https://example.com/photo.jpg | serpapi search engine=google_lens --image -
# Keep only the visual matches
serpapi search engine=google_lens --image ./photo.jpg --jq ".visual_matches[:5]"
Supported formats are JPG/JPEG, PNG and WebP up to 500 KB. See the Google Lens image upload docs.
archive
Fetches an earlier search by its search_metadata.id, for free. A search made with async=true has search_metadata.status Queued or Processing until it is Success, so poll. output= takes the same values as in search.
serpapi archive 6a2b3c4d5e6f7a8b9c0d1e2f
serpapi archive 6a2b3c4d5e6f7a8b9c0d1e2f output=md
locations
Finds values for the location search parameter: pass a result's canonical_name. Free, and needs no API key. limit is at most 10. A query is required, because without one the API returns every location (66 MB).
serpapi locations q=Austin limit=3 --jq '.[0].canonical_name'
account
Shows the plan, usage and searches left, for free. The API key in the output is masked.
serpapi account --jq .total_searches_left
image
Upload an image with the Image API and print its image_id without searching. Useful when you want to reuse the same upload across several engine=google_lens searches, the only engine that takes an image_id; image IDs expire after 10 minutes. For a one-off search, prefer serpapi search engine=google_lens --image <file>.
# Upload an image file
serpapi image ./photo.jpg
# Read the image from stdin
curl -s https://example.com/photo.jpg | serpapi image -
# Reuse the image_id across searches
IMAGE_ID=$(serpapi image ./photo.jpg --jq .image_id)
serpapi search engine=google_lens image_id=$IMAGE_ID
serpapi search engine=google_lens image_id=$IMAGE_ID type=exact_matches
login
Checks an API key with a free account request, then saves it. The key comes from --api-key, a hidden prompt on a terminal, or else the first line of stdin, which login waits for.
serpapi login
printf %s "$KEY" | serpapi login
version
Prints the CLI version, as --version does.
Global flags
-
--jq <expr>: jq filter applied to the JSON result, likegh --jq; strings print unquoted andnullas an empty line, and results stream as they are produced. A filter that prints onlynullgets a warning listing the response's top-level keys. jq is built in, so nothing else needs installing. In bash and zsh, quote the expression with single quotes. -
--fields <expr>: search only; see search. -
--api-key <key>: API key. PreferSERPAPI_KEY, since flags show up inpsand shell history. -
--timeout <seconds>: HTTP request timeout in seconds, such as120, or a duration, such as2m(default60; envSERPAPI_TIMEOUT).0waits indefinitely. Slow engines such asgoogle_ai_mode, and searches withno_cache=true, can take longer than the default; raise it when anetwork_errorsays the request timed out, and add--debugto see which step stalled. A timed-out search may still finish on SerpApi's side, count against your searches and be available in your search archive.# Give an AI Mode query up to two minutes serpapi search --timeout 120 engine=google_ai_mode q="..." # Or set it once for the shell session export SERPAPI_TIMEOUT=120 -
--debug: trace each request on stderr (envSERPAPI_DEBUG=1): DNS, TCP connect (local and remote address), TLS handshake, time to first byte, response status and headers (includingSerpapi-Search-IdandX-Request-Id), body size, and, for searches, the server'ssearch_metadatatimeline correlated with the client's, so you can see whether a long wait happened on the network path before SerpApi received the request, during processing, or while the response travelled back. Each line carries a UTC timestamp and the seconds elapsed since the request started; the API key is redacted; stdout is untouched, so--jqand piping still work. On a terminal the trace is plain text:
Piped or redirected, stderr stays one JSON object per line, so scripts that parse it keep working; headers and the timeline travel as structured[debug] 2026-10-09T11:50:54.272Z + 0.000s GET https://serpapi.com/search.json?api_key=[REDACTED]&engine=google&q=coffee [debug] 2026-10-09T11:50:54.272Z + 0.000s Timeout: 60s [debug] 2026-10-09T11:50:54.300Z + 0.028s DNS resolved: 162.159.142.21, 172.66.2.17 [debug] 2026-10-09T11:50:54.346Z + 0.074s TLS handshake done: TLS 1.3, ALPN="h2" [debug] 2026-10-09T11:50:54.413Z + 0.141s Using connection: 192.168.1.20:62494 -> 162.159.142.21:443 (new) [debug] 2026-10-09T11:50:54.414Z + 0.142s Request sent, waiting for response headers [debug] 2026-10-09T11:50:55.227Z + 0.955s First response byte received [debug] 2026-10-09T11:50:55.227Z + 0.955s Response: HTTP/2.0 200 OK [debug] 2026-10-09T11:50:55.227Z + 0.955s Serpapi-Search-Id: 6ac8d51eaf5fdcac5434ae91 [debug] 2026-10-09T11:50:55.234Z + 0.962s Body received: 57483 bytes [debug] 2026-10-09T11:50:55.234Z + 0.962s Server search: id=6ac8d51eaf5fdcac5434ae91 status=Success total_time_taken=0.5s [debug] 2026-10-09T11:50:55.234Z + 0.962s Server timeline: created_at=11:50:54Z (+0.0s after request sent), processed_at=11:50:54Z (+0.0s processing), headers received +1.2s after processed_atdata:
Server timestamps have one-second resolution, so sub-second deltas in the timeline are noise. When the client waited noticeably longer than{"debug":{"at":"2026-10-09T11:50:54.414Z","elapsed":0.142,"event":"Request sent, waiting for response headers"}} {"debug":{"at":"2026-10-09T11:50:55.227Z","elapsed":0.955,"event":"Response: HTTP/2.0 200 OK","data":{"status":200,"proto":"HTTP/2.0","headers":{"Serpapi-Search-Id":"6ac8d51e..."}}}} {"debug":{"at":"2026-10-09T11:50:55.234Z","elapsed":0.962,"event":"Server timeline: ...","data":{"sent_to_created_s":0.0,"processing_s":0.0,"processed_to_headers_s":1.2}}}total_time_taken, aNote:event points at the phase that dominated, if this machine's clock agrees with SerpApi's: it says so, and names no phase when the timestamps contradict each other. To send a trace along with a support request:SERPAPI_DEBUG=1 serpapi search ... > result.json 2> trace.jsonl.
Configuration
The API key comes from the first of:
--api-keySERPAPI_KEYSERPAPI_API_KEY- the key saved by
serpapi login
Without a key, search still runs, but the API answers only a few demo queries such as q=coffee and returns 401 for the rest. account and archive need a key.
serpapi login saves the key, readable only by you, to serpapi/config.toml in the OS config directory: $XDG_CONFIG_HOME or ~/.config on Linux, ~/Library/Application Support on macOS, %AppData% on Windows. Without an absolute HOME (%AppData% on Windows) there is no config file: login exits 2, and other commands need --api-key or SERPAPI_KEY.
api_key = "your_serpapi_key_here"
A config file that can't be read or isn't valid TOML is reported by path and line, never by content: search warns and runs without a key, while account and archive stop. serpapi login rewrites it.
For AI agents
serpapi --help is the full contract. In short:
-
stdout carries only results: JSON indented by two spaces, colored on a terminal. Piped, it is indented too, so
head,grepand file viewers show whole lines;headcuts the JSON short, so take parts with--jq. A terminal gets control characters from results escaped, so a page can't drive it; piped output keeps them. Shrink them on the server with--fields, then locally with--jq, or ask foroutput=md. -
Unfamiliar engine? Responses are often 20–300 KB, and one key can be large too (Maps
place_resultsholds reviews and photos). Piped without--jq, a result over 16 KB comes with a warning on stderr that lists its top-level keys by size, unless stderr goes into the same pipe (2>&1), where it would break a JSON parser. Print a sample with one item per array,--jq 'walk(if type == "array" then .[:1] else . end)'; nested arrays are cut too, so it shows the shape, not the answer. Then take what you need from the stored search for free:serpapi archive SEARCH_METADATA_ID --jq '...'. -
Errors and warnings go to stderr, one JSON object per line; so does the
--debugtrace, as{"debug":{...}}lines, when stderr is not a terminal:{"error":{"code":"api_error","message":"Invalid API key. ...","status":401}} {"warning":{"message":"..."}} {"debug":{"at":"2026-10-09T11:50:54.414Z","elapsed":0.142,"event":"Request sent, waiting for response headers"}} -
The exit code says what to do next:
Exit codeMeaning 0 Success; warnings may still be on stderr. 2 usage_errorThe command is wrong; fix it before retrying. 1 api_errorstatusis the HTTP status: 200 the search ran but reported a problem, such as no results; 400 invalid parameters; 401 invalid API key; 403 account not allowed; 429 rate limited or out of searches; 5xx SerpApi failed. Nostatus: the response was unreadable.1 network_errorNo answer, such as a timeout. 130 interruptedCtrl-C or SIGTERM stopped it before it finished, after printing what had arrived, such as the --all-pagespages fetched so far. -
Cost: each successful search counts against your monthly searches, including each
--all-pagespage; cached, errored and failed ones don't, though errored ones count toward the hourly limit (pricing FAQ). An identical search,--fieldsincluded, repeated within an hour comes from the cache.no_cache=trueforces a new, counted search; cached results are at most an hour old, so add it only when that is too old.zero_trace=true(Enterprise) stores nothing, so it skips the cache too and leaves no search forarchiveto read.account,locationsandarchiveare free, and so is animageupload, though it needs searches left this month and this hour, and has its own hourly limit,account_rate_limit_per_hourinserpapi account. -
No prompts without a terminal. Only
serpapi loginand an image given as-(serpapi image -,search --image -) read stdin.loginwaits for the first line, so pipe the key in (printf %s "$KEY" | serpapi login) or, simpler, setSERPAPI_KEY; an image is read until stdin closes.