Alle artikelen

· 7 min. leestijd

SERP API: A Practical Guide with Code Examples

Scraping search results yourself works until it doesn't. Markup changes, CAPTCHAs appear, and you end up maintaining parsers and proxy pools instead of building your product. A SERP API handles the fetching and parsing and gives you structured JSON. This guide walks through the serpfast SERP API end to end: authentication, parameters, response fields, code in three languages, error handling with retries, and a few habits that keep costs predictable.

What a SERP API does

A SERP (search engine results page) API takes a query plus settings like country and language, runs the search live and returns the results as structured data. Instead of HTML you get an array of organic results with positions, titles, URLs and snippets, plus extras like related searches.

Typical uses include rank tracking, competitor monitoring, content research, feeding fresh web results into an LLM application, and building SEO tools.

Making a request

The organic search endpoint is GET https://serpfast.com/api/v1/search. You can authenticate in any of three ways:

  • An api_key query parameter: convenient for quick tests in a browser or curl.
  • An Authorization: Bearer YOUR_KEY header.
  • An X-API-Key: YOUR_KEY header.

Headers are the better choice in production, because URLs (including query parameters) tend to end up in logs.

Request parameters

ParameterRequiredDescription
qYesThe search query, up to 400 characters.
glNoTwo-letter country code for the market, such as US, GB or DE. More than 40 markets are supported.
hlNoLanguage code, such as en or de.
numNoNumber of results, from 10 to 100.
pageNoWhich page of num results to return, starting at 1.
timeNoRestrict by freshness: d (past day), w (week), m (month), y (year).
relatedNoSet to true to include related searches in the response.

Billing is simple: 1 credit per 10 results. A request with num=10 uses 1 credit, num=30 uses 3 and num=100 uses 10.

Response fields

A trimmed response for q=headless+cms:

{
  "search_metadata": {
    "id": "srch_8f2c1a7e",
    "status": "success",
    "endpoint": "search",
    "credits_used": 1,
    "credits_remaining": 4999,
    "created_at": "2026-09-30T10:15:00.000Z"
  },
  "search_parameters": {
    "q": "headless cms",
    "gl": "US",
    "hl": "en",
    "page": 1,
    "num": 10,
    "time": null
  },
  "search_information": {
    "total_results": 1250000,
    "time_taken_ms": 1840
  },
  "organic_results": [
    {
      "position": 1,
      "title": "What is a headless CMS?",
      "link": "https://example.com/headless-cms",
      "displayed_link": "example.com > headless-cms",
      "snippet": "A headless CMS stores content and delivers it through an API...",
      "date": "Aug 12, 2026"
    }
  ],
  "related_searches": [
    { "query": "headless cms comparison" },
    { "query": "open source headless cms" }
  ]
}

Organic results

  • position: rank among organic results, starting at 1.
  • title: the result's title as shown on the results page.
  • link: the full destination URL.
  • displayed_link: the breadcrumb-style URL shown under the title.
  • snippet: the description text.
  • date: the date shown for the result, when there is one. Treat it as optional.

Metadata, parameters and related searches

search_metadata contains a request id (log it, it helps with support), the status, the endpoint, credits_used for this call, credits_remaining on your account and a created_at timestamp. search_parameters echoes the settings that were actually applied, which is useful when you rely on defaults. search_information holds the estimated total result count (when available) and timing. related_searches is an array of { "query": "..." } objects, a good source of keyword ideas.

Code examples

curl

curl "https://serpfast.com/api/v1/search?q=headless+cms&gl=US&hl=en&num=10&related=true" \
  -H "X-API-Key: $SERPFAST_API_KEY"

Node.js (fetch)

Node 18 and later ship with fetch, so no dependencies are needed.

const params = new URLSearchParams({
  q: "headless cms",
  gl: "US",
  hl: "en",
  num: "20",
  related: "true",
});

const res = await fetch("https://serpfast.com/api/v1/search?" + params, {
  headers: { Authorization: "Bearer " + process.env.SERPFAST_API_KEY },
});
if (!res.ok) {
  const body = await res.json().catch(() => null);
  throw new Error("serpfast " + res.status + ": " + (body?.error?.message ?? "request failed"));
}

const data = await res.json();
for (const r of data.organic_results) {
  console.log(r.position, r.title, r.link);
}
console.log("Credits left:", data.search_metadata.credits_remaining);

Python (requests)

import os
import requests

params = {
    "q": "headless cms",
    "gl": "US",
    "hl": "en",
    "num": 20,
    "time": "m",
}
headers = {"Authorization": "Bearer " + os.environ["SERPFAST_API_KEY"]}

resp = requests.get("https://serpfast.com/api/v1/search", params=params, headers=headers, timeout=30)
resp.raise_for_status()
data = resp.json()

for r in data["organic_results"]:
    print(r["position"], r["title"], r["link"])

for rs in data.get("related_searches", []):
    print("related:", rs["query"])

Passing params instead of building the URL by hand means queries with spaces, ampersands or non-ASCII characters are encoded correctly.

Handling errors

Errors come back with a standard HTTP status and a JSON body of the form {"error": {"code": "...", "message": "..."}}. Branch on code, and show or log message, which explains what went wrong.

StatusCodeWhat to do
400invalid_requestA parameter is missing or out of range (for example num=500). Fix the request. Do not retry.
401missing_api_key, invalid_api_keyThe key is missing, mistyped or revoked. Fix the configuration. Do not retry.
402insufficient_creditsYour balance is too low for the request. Top up or lower num. Retrying will not help.
429rate_limitedYou are sending requests too fast. Wait for the number of seconds in the Retry-After header, then retry.
502upstream_errorThe search could not be completed. Retry after a short delay.
503service_unavailableTemporarily unavailable. Retry with backoff.
504upstream_timeoutThe search took too long. Retry with backoff.

Failed requests are refunded, so a 502 or 504 does not cost you credits. A small retry helper in Node.js that honors Retry-After and backs off exponentially otherwise:

const RETRYABLE = new Set([429, 502, 503, 504]);

async function searchWithRetry(params, attempts = 4) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch("https://serpfast.com/api/v1/search?" + new URLSearchParams(params), {
      headers: { "X-API-Key": process.env.SERPFAST_API_KEY },
    });
    if (res.ok) return res.json();

    const body = await res.json().catch(() => null);
    const code = body?.error?.code ?? "unknown";
    if (!RETRYABLE.has(res.status) || i === attempts - 1) {
      throw new Error(res.status + " " + code + ": " + (body?.error?.message ?? ""));
    }

    const retryAfter = Number(res.headers.get("retry-after"));
    const delayMs = retryAfter > 0 ? retryAfter * 1000 : 1000 * 2 ** i; // 1s, 2s, 4s
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
}

Practical tips

Pagination

You have two options for deeper results: ask for more in one call with num (up to 100), or walk pages with page. Both cost the same per 10 results. Use num when you always need the top 50. Use page when you can stop early, for example a script that stops as soon as it finds a target domain. For that specific job, the /api/v1/rank endpoint already does the scanning for you.

Cache on your side

Search results for most queries do not change minute to minute. Cache responses keyed on the full parameter set (q, gl, hl, num, page, time, related) with a lifetime that matches your use case: a few hours for content research, a day for rank tracking. Normalizing the query (trim whitespace, lowercase) before building the cache key avoids paying twice for "Headless CMS" and "headless cms".

Respect rate limits

For batch jobs, run requests through a small concurrency limiter rather than firing hundreds at once. A steady pace finishes faster than a burst that triggers 429s and backoff. Spread scheduled jobs over the hour instead of starting everything at :00.

Watch your balance

Read search_metadata.credits_remaining on each response and alert when it drops below a threshold, so a long-running job does not stop halfway with 402 errors.

Store what you need

Keep the raw JSON for a while, along with the search_metadata.id. If a parser change on your side breaks something, you can reprocess stored responses instead of paying for the searches again.

Other endpoints

The same key works for /api/v1/images (image results in images_results, 1 credit per request), /api/v1/autocomplete (search suggestions in suggestions, 1 credit per request) and /api/v1/rank (find where a domain ranks for a query, 1 credit per 10 results scanned). You can try each one without code using the free tools: SERP Checker, Image Search, Keyword Suggestions and Rank Checker.

Get started

Sign in with Google to get 5,000 free credits, one time and with no credit card required. That is enough for 5,000 searches of 10 results. The full parameter reference is in the docs, and plan details are on the pricing page.

Deze data nodig in je app?

Dezelfde resultaten zijn beschikbaar via de API, met 5.000 gratis credits.

Gratis API-sleutel aanvragen