Search engine results as JSON from SerpApi, for people, scripts and AI agents
brew tap serpapi/homebrew-tap
brew install serpapi-cliDownload directly from GitHub Releases
go install github.com/serpapi/serpapi-cli/cmd/serpapi@latestThis needs Go 1.25 or newer; with the default GOTOOLCHAIN=auto, Go 1.21 and later download it on their own.
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 # Markdownserpapi --help and each command's --help document parameters, errors and costs.
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.
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=mdFinds 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'Shows the plan, usage and searches left, for free. The API key in the output is masked.
serpapi account --jq .total_searches_leftChecks 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 loginPrints the CLI version, as --version does.
--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. 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
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.
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:
{"error":{"code":"api_error","message":"Invalid API key. ...","status":401}} {"warning":{"message":"..."}} -
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. -
No prompts without a terminal. Only
serpapi loginreads stdin, and it waits for the first line, so pipe the key in (printf %s "$KEY" | serpapi login) or, simpler, setSERPAPI_KEY.