Wiring Live Search into n8n Without Writing a Custom Node

n8n's HTTP Request node is enough to give any workflow real search results. The tricky parts are the path format and the two response shapes.

Profile picture of Serply
Serply
An n8n workflow calling a search API and branching on results

n8n users reach for a community node when they need search, and then discover it’s unmaintained or missing the endpoint they want. The HTTP Request node does the whole job. You just have to get three details right.

Credentials, once

Create a Header Auth credential rather than pasting the key into every node:

  • Name: X-Api-Key
  • Value: your Serply key

Every HTTP Request node then selects Generic Credential Type → Header Auth and picks it. When the key rotates you change one record, not fourteen nodes. This also keeps the key out of the workflow JSON, which matters the moment someone exports a workflow to share it.

The path format catches everyone

Serply embeds the query in the URL path, not in a query string. In an HTTP Request node that means the whole thing goes in the URL field with no separate query parameters:

https://api.serply.io/v1/search/q={{ encodeURIComponent($json.query) }}&num=10

Not the Parameters table. If you add q as a query parameter, n8n appends ?q=... and you’ll get a 404 or an empty result set, depending on the endpoint.

encodeURIComponent in the expression is not optional. A query containing &, #, or + will otherwise break the path in ways that produce results for a different query — which is worse than an error, because nothing fails.

Method GET, URL as above, Authentication the header credential. Optionally add headers:

HeaderValue
X-Proxy-LocationUS, GB, DE, JP, …
X-User-Agentdesktop or mobile

The response is JSON with hits under results. To fan out one item per result, add an Item Lists node (or a Code node) splitting on results:

return $input.first().json.results.map(r => ({
  json: {
    title: r.title,
    url: r.link,
    snippet: r.description,
    position: r.position,
  },
}));

Each result also carries realPosition — the position on the page counting SERP features — and a metadata.display_url. If your workflow reports rank to anyone, decide which of position and realPosition you mean and be consistent about it.

Node two: news is shaped differently

This is the second thing that trips people. The news endpoint does not return results:

https://api.serply.io/v1/news/q={{ encodeURIComponent($json.topic) }}

Articles arrive under feed.entries, each with title, link, summary, published, and source. A Code node to normalise both into one shape:

const body = $input.first().json;
const items = body.results ?? body.feed?.entries ?? [];

return items.map(i => ({
  json: {
    title: i.title,
    url: i.link,
    text: i.description ?? i.summary ?? '',
    published: i.published ?? null,
  },
}));

Dropping that node between the request and the rest of the workflow means downstream branches don’t care which endpoint fed them.

Node three: reading a page

For workflows that need the actual content — summarising an article, extracting a price, feeding a model — POST to the scraper:

Method POST, URL https://api.serply.io/v1/request, Body Content Type JSON:

{
  "url": "={{ $json.url }}",
  "response_type": "markdown"
}

Set Response → Format to String for markdown mode. The API returns the text as the body rather than wrapped in JSON, and n8n’s default JSON parsing will fail on it. If you’d rather keep JSON parsing everywhere, use "response_type": "full" instead and read data — that returns raw HTML inside a JSON envelope.

Handling the failure cases

Turn on Settings → Always Output Data and Continue On Fail for the search node, then branch on status with an IF node reading $json.error. The statuses you’ll actually see:

  • 429 — rate limited. Wire this to a Wait node and loop back. The response carries x-ratelimit-requests-remaining, so you can also check it proactively and slow the workflow before you hit the wall.
  • 422 — malformed request, almost always a missing encodeURIComponent.
  • 404 — usually the ?q= mistake above.

For scheduled workflows the 429 branch matters more than it seems. A cron that fires hourly and silently fails on rate limits looks identical to a cron that finds nothing, and you won’t notice for weeks.

A workflow worth building first

The pattern that pays off immediately: Schedule Trigger (daily) → HTTP Request (news for your brand) → Code (normalise, filter to items published since the last run) → IF (any new items?) → HTTP Request (scrape each article) → AI node (summarise and classify sentiment) → Slack.

The “published since last run” filter is what makes it usable rather than annoying. Store the last-run timestamp in n8n’s static data:

const staticData = $getWorkflowStaticData('global');
const since = staticData.lastRun ? new Date(staticData.lastRun) : new Date(0);
const items = $input.all().filter(i => new Date(i.json.published) > since);
staticData.lastRun = new Date().toISOString();
return items;

Without it, every run re-summarises the same fifteen articles and the channel gets muted within a week.