Google Search Scraper API: Five Defaults That Return Empty Data
Lead Scraping Automation Engineer
TL;DR:
- A
200from the Google Search actor is not proof of data: the response can carry an emptyorganic_resultsarray, an ad placeholder instead of a listing, or a field that is blank by design. - Dify pre-fills two API-key defaults that both produce
401with{"code":14404,"message":"invalid access token"}— the header name defaults toAuthorizationand the header prefix defaults toBasic, and the actor accepts neither. - An n8n workflow can validate with zero errors and still fail at run time, because the Code node sandbox in version 2.34.4 does not expose the global
URLconstructor. - An agent given a search tool can answer without calling it, producing fluent text that never touched the API; counting tool calls turns that silent miss into a failure.
- Local-pack records return
place_id,gps_coordinates, andthumbnailempty, andphone,type, andhourswith a leading space — both are documented behavior, not faults to debug.
What the Google Search actor returns
The scraper.google.search actor takes a query and returns a parsed SERP as JSON. It is the Google surface of Deep SerpApi, and it is usually the first actor wired into a workflow builder or an agent framework, because a ranked result list feeds rank tracking and lead research equally well.
The failures below are not exotic. They come from the gap between a request that is accepted and a payload that is usable — and every one of them can be reproduced from the four host platforms this article uses as examples: Dify, n8n, Activepieces, and LangChain.
The request: endpoint, actor, and parameters
Every call is a POST to a single endpoint with two fields. actor selects the scraper and input carries its parameters:
bash
curl -sS -X POST https://api.scrapeless.com/api/v1/scraper/request \
-H 'Content-Type: application/json' \
-H "x-api-token: $SCRAPELESS_API_KEY" \
-d '{"actor":"scraper.google.search","input":{"q":"web scraping api"}}'
Three parameters cover most work:
| Parameter | Purpose |
|---|---|
q |
The query string. |
tbm |
Result type. lcl returns the local pack instead of web results. |
start |
Result offset for pagination — 20 per page on the local pack. |
The authentication header is x-api-token. That name is the field a no-code platform is most likely to fill with its own default. The HTTP semantics specification for 401 responses expects a challenge tied to the resource's own authentication scheme, so a platform that assumes Authorization is being reasonable — it is simply assuming the wrong scheme for this endpoint.
The response envelope
Read the envelope before you read the data. A successful Google Search call returns these top-level keys:
json
// illustrative sample — key shape only; values omitted
{
"search_information": {},
"organic_results": [],
"related_searches": [],
"pagination": {},
"metadata": {}
}
Two things follow from that shape. There is no success flag to branch on, so the presence and length of organic_results is the signal. And there is no People-Also-Ask block in this envelope — a query that shows related questions in a browser returns related_searches here, so a workflow that expects a questions array gets None and writes a blank column.
With tbm set to lcl, results move to local_results.places[] instead of organic_results[]. A pipeline that hard-codes one path silently produces nothing when the other is requested.
Reading the response in code
The assertion after the request is the part worth copying. This example raises instead of returning an empty list, so a failure surfaces where it happened rather than three steps later in a spreadsheet:
python
import json
import os
import urllib.request
ENDPOINT = "https://api.scrapeless.com/api/v1/scraper/request"
def search(query: str) -> dict:
payload = json.dumps({"actor": "scraper.google.search", "input": {"q": query}}).encode()
request = urllib.request.Request(
ENDPOINT,
data=payload,
headers={
"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
},
)
# urlopen raises HTTPError on any 4xx or 5xx, so a rejected call never reaches the parser.
with urllib.request.urlopen(request, timeout=120) as response:
return json.loads(response.read())
serp = search("web scraping api")
organic = serp.get("organic_results") or []
if not organic:
raise SystemExit(f"no organic_results in the response; envelope was {sorted(serp)}")
print(f"organic_results: {len(organic)}")
print(f"first result: {organic[0]['title']}")
print(f"envelope keys: {sorted(serp)}")
Absent and empty are different states, and the JSON interchange format specification gives you no help distinguishing "the key was omitted" from "the value is an empty string". Decide which one your pipeline treats as an error before you write the first insert.
Working through this on the free plan is enough to see every behavior described here — create a Scrapeless account and use the same key across all four platforms below.
Five defaults that return empty data
The API-key header your platform pre-fills is the wrong one
In Dify 1.16.1, importing an OpenAPI schema as a custom tool and choosing API Key authentication leaves two fields at defaults the actor rejects. The header name defaults to Authorization, and the header prefix defaults to Basic — which sends x-api-token: Basic <key> even after you correct the name. Both produce the same response:
json
{ "code": 14404, "message": "invalid access token" }
One error message, two independent causes, which is what makes it expensive to diagnose. The working configuration names all three:
| Field | Value |
|---|---|
| Auth type | API Key |
| Header name | x-api-token |
| Header prefix | Custom |
Dify also flattens a nested request-body object into a string parameter, so the input field arrives as text rather than as a structured object. Both an object and a JSON string are accepted, which is why this one rarely gets noticed until a downstream node tries to read input.q.
A workflow that validates can still fail at run time
Static validation and execution disagree in the n8n Code node. A workflow using new URL(link).hostname to group results by domain validates with zero errors, then fails on the first item with URL is not defined. The sandbox in version 2.34.4 does not expose that global, even though the WHATWG URL Standard defines it as a Web API constructor and n8n's own report of the Code node failing without the URL constructor records the symptom.
Derive the hostname with string operations instead:
javascript
// The Code node sandbox does not expose the global URL constructor,
// so the hostname comes from string operations.
const hostname = (link) =>
link ? link.replace(/^[a-z]+:\/\//i, '').replace(/^www\./i, '').split(/[/?#]/)[0] : '';
const results = [
{ position: 1, link: 'https://www.scrapeless.com/en/product/deep-serp-api' },
{ position: 2, link: 'https://docs.scrapeless.com/en/deep-serp-api/quickstart/introduction/' },
];
for (const result of results) {
console.log(result.position, hostname(result.link));
}
Validation in a workflow builder checks the graph, not the code inside a node. A green check therefore says nothing about whether a Code node will execute.
The step reference that resolves to nothing
In Activepieces 0.82.0, an HTTP step's parsed JSON lives under body. The reference is {{step_1.body.organic_results}}, and {{step_1.organic_results}} resolves to nothing at all — no error, no warning, just an empty loop and a run that reports success. With tbm set to lcl, the path is {{step_1.body.local_results.places}}.
A missing-reference failure looks identical to a genuinely empty result set, so check the reference path before you go looking for a data problem.
The agent that answers without calling the tool
Give an agent a search tool and it may not use it. A small model handed both a search tool and a fetch tool will often run the search, then answer from the result snippets while describing what "the page says" — never fetching the page. The prose is fluent and the citation is implied, so nothing in the output marks the answer as ungrounded.
The fix is an assertion, not a better prompt. Count tool calls and treat zero as a failure:
Note: this snippet wraps an existing agent, so running it requires a constructed LangChain agent and a model-provider key. Everything it depends on is standard
agent.stream(...)output.
python
tool_calls = 0
for chunk in agent.stream({"messages": [("human", question)]}, stream_mode="values"):
message = chunk["messages"][-1]
tool_calls += len(getattr(message, "tool_calls", None) or [])
if tool_calls == 0:
raise SystemExit("the model answered without calling a tool; the answer is not grounded")
Single-tool instructions are reliable on small models. Chained instructions — search, then fetch the top result — are where the tool call quietly goes missing, so split the steps in code and let the model handle one call at a time.
Fields that are empty on purpose
Some blank values are correct. In local-pack results, place_id, gps_coordinates, and thumbnail come back empty, and phone, type, and hours arrive with a leading space. Neither is a fault, and both break naive code: a trailing-space mismatch turns a deduplication key into a duplicate, and treating an empty place_id as an error sends you debugging behavior that is working as documented.
Normalize on the way in:
| Field | Behavior | Handling |
|---|---|---|
phone, type, hours |
Leading space | Trim before storing or comparing. |
place_id, gps_coordinates, thumbnail |
Empty on local results | Treat as nullable; do not gate the record on them. |
organic_results vs local_results.places |
Depends on tbm |
Select the path from the request, not by guessing. |
The same discipline applies to counting. A result array can contain sponsored slots and layout placeholders alongside listings, so the length of the array is not the number of results — filter on the record's own type field before you report a count, or every number downstream inherits whatever ad load the page happened to serve.
Conclusion
The costly failures in a no-code scraping setup finish green with nothing in them: an auth header your platform pre-filled, a Web API global the sandbox omits, a reference path missing one segment, an agent that skipped the tool, or a field that was always going to be blank. Each has a one-line fix and no error message pointing at it.
Two habits catch all five. Read the response envelope before the data, and assert on what you expect — a non-empty array, a tool call, a record type — so a silent miss becomes a loud failure at the step that caused it. The n8n scraping workflow guide and the LangChain integration walkthrough show the same actor wired end to end once those checks are in place.
Ready to build against a SERP surface that returns a documented envelope? Check the Deep SerpApi documentation for the full parameter set, review plans and included volume, and start on the free plan.
FAQ
Q: Why does my Google Search actor call return 200 with an empty organic_results array?
An empty organic_results array with a 200 means the request was accepted and parsed but produced no web results for that query shape. Check three things in order: whether tbm was set to lcl, which moves results to local_results.places[]; whether the query itself has result intent; and whether your platform is reading the parsed body rather than the envelope. There is no success flag in the response, so the array's length is the only signal.
Q: What causes {"code":14404,"message":"invalid access token"} when the key is correct?
That response means the key never arrived in the form the endpoint expects. The header must be x-api-token carrying the bare key. Platforms that default to Authorization, or that prepend Basic or Bearer to the value, send a header the endpoint cannot read — and the message is identical in every case, so verify the header name and any prefix setting separately.
Q: Why does my n8n Code node fail with URL is not defined when the workflow validates?
The Code node sandbox in n8n 2.34.4 does not expose the global URL constructor, and workflow validation does not execute node code, so the graph passes its checks and the run fails on the first item. Parse the hostname with string operations, or move the URL handling into a node that provides the API.
Q: How do I tell whether an agent actually used the search tool?
Count the tool calls on the streamed messages and fail when the count is zero. A model can produce a complete, confident answer without invoking any tool, and nothing in the text distinguishes that from a grounded answer. Treat the tool-call count as a hard requirement rather than inspecting the prose.
Q: Are empty place_id and gps_coordinates values a bug?
No. Local-pack records return place_id, gps_coordinates, and thumbnail empty, so those fields are nullable by design. Keep the record and populate location from the fields that are present rather than discarding rows or adding error handling around expected behavior.
Q: Why does my Activepieces loop iterate zero times when the HTTP step succeeded?
The parsed response is nested under body, so {{step_1.organic_results}} resolves to nothing while {{step_1.body.organic_results}} resolves to the array. A missing reference produces no error in Activepieces 0.82.0 — the loop simply receives nothing and the run still reports success, which makes it indistinguishable from an empty result set until you check the path.
At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.



