# SERP API in Node.js: fetch, a worker pool and pagination

> Call the SerpCrypto SERP API from Node.js 18+ with the built-in fetch: a typed client, a worker pool under your concurrency limit and a pagination loop.

- URL: https://serpcrypto.com/docs/node/
- Last updated: 2026-10-22
- Publisher: SerpCrypto (https://serpcrypto.com)

No dependency: Node.js has had `fetch` since version 18. A small client with JSDoc types for editors and TypeScript, a pool that respects your account's limit, and a pagination loop that stops when the results run out.

**Quick answer — How do I use a SERP API in Node.js?**

Call `fetch("https://serpcrypto.com/api/v1/search?" + new URLSearchParams({ q }))` with the header `Authorization: Bearer <key>` and read `await res.json()`. Node.js 18+ needs no package. **SerpCrypto** returns Google or Bing results as JSON for 1 credit per successful search, from $0.30/1K searches, and never charges errors. Paid in crypto, no KYC.

## Node.js setup at a glance

| Attribute | Value |
| --- | --- |
| Runtime | Node.js 18 or later (built-in `fetch`, `AbortSignal.timeout`) |
| Modules | ES modules: `.mjs` files or `"type": "module"` in package.json |
| Key | `process.env.SERPCRYPTO_API_KEY` |
| Timeout | 90 seconds with `AbortSignal.timeout(90_000)` |
| Parallel searches | A pool sized to `concurrency_limit` |
| Types | JSDoc typedefs, read by VS Code and `tsc --checkJs` |

## How do I write a small typed client?

Put the endpoint, the header and the error handling in one module. The JSDoc `@typedef` lines describe the answer, so editors autocomplete `organic_results` and type-check your code without a build step. TypeScript users can copy the same shapes into an interface.

The `search` function throws an `Error` with the HTTP status and the API's error `code` in its message. Your code can then decide to retry `429`, `502` and `504` and to stop on the others, as the [errors page](https://serpcrypto.com/docs/errors/) explains.

*serp-client.mjs*

```javascript
// serp-client.mjs — Node.js 18+, no dependency
const API_URL = "https://serpcrypto.com/api/v1/search";
const CREDITS_URL = "https://serpcrypto.com/api/v1/credits";
const headers = { Authorization: `Bearer ${process.env.SERPCRYPTO_API_KEY}` };

/**
 * @typedef {Object} OrganicResult
 * @property {number} position
 * @property {string} title
 * @property {string} link
 * @property {string} [snippet]
 * @property {string} [date]
 */

/**
 * @typedef {Object} SearchResponse
 * @property {Object} search_metadata
 * @property {string} search_metadata.id
 * @property {string} search_metadata.status
 * @property {number} search_metadata.credits_used
 * @property {OrganicResult[]} [organic_results]
 * @property {Object} [pagination]
 * @property {number} pagination.current
 * @property {number} [pagination.next_page]
 */

/**
 * Run one search (1 credit when it succeeds).
 * @param {string} q
 * @param {Record<string, string | number>} [params]
 * @returns {Promise<SearchResponse>}
 */
export async function search(q, params = {}) {
  const query = new URLSearchParams({ q, ...Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)])) });
  const res = await fetch(`${API_URL}?${query}`, { headers, signal: AbortSignal.timeout(90_000) });
  if (!res.ok) {
    const body = await res.json().catch(() => ({}));
    const err = new Error(`${res.status} ${body.error?.code ?? "error"}: ${body.error?.message ?? ""}`);
    err.status = res.status;
    throw err;
  }
  return res.json();
}

/** Free: credits left, used today and the concurrency limit. */
export async function credits() {
  const res = await fetch(CREDITS_URL, { headers });
  if (!res.ok) throw new Error(`credits: HTTP ${res.status}`);
  return res.json();
}
```

## How do I run searches in parallel without hitting 429?

Start as many workers as your account allows, and let each one take the next keyword from a shared list. The pool reads `concurrency_limit` from the free credits endpoint, so it never sends more searches at once than the account accepts.

A failed keyword is recorded and the worker moves on. Re-run the failures afterwards: errors cost nothing, so a second pass is free until it succeeds.

*batch.mjs: a pool of workers*

```javascript
// batch.mjs — parallel searches, never more than the account's limit at once
import { search, credits } from "./serp-client.mjs";

const keywords = ["password manager", "note taking app", "time tracking software", "invoice software"];
const { concurrency_limit: limit } = await credits();

let next = 0;
const results = new Map();

async function worker() {
  while (next < keywords.length) {
    const kw = keywords[next++];
    try {
      const data = await search(kw, { gl: "us", hl: "en" });
      results.set(kw, data.organic_results?.[0]?.link ?? null);
    } catch (err) {
      results.set(kw, `failed: ${err.message}`);
    }
  }
}

await Promise.all(Array.from({ length: Math.min(limit, keywords.length) }, worker));
console.table(Object.fromEntries(results));
```

## How do I fetch several pages of results?

Loop over `page` and stop as soon as `pagination.next_page` is missing. About 10 organic results per page, as Google has served since it stopped honouring num=100 in September 2025; each extra page is one more credit. A three-page check of one keyword costs three credits; a keyword with only one page of results costs one.

*pages.mjs*

```javascript
// pages.mjs — up to three pages, stop when results end
import { search } from "./serp-client.mjs";

async function topResults(q, pages = 3, params = {}) {
  const rows = [];
  for (let page = 1; page <= pages; page++) {
    const data = await search(q, { ...params, page });
    for (const r of data.organic_results ?? []) rows.push({ page, position: r.position, link: r.link });
    if (!data.pagination?.next_page) break; // nothing more: save the credits
  }
  return rows;
}

console.log(await topResults("headless cms", 3, { gl: "de", hl: "en" }));
```

## Can I call the API from a browser or an edge function?

From a server or an edge function, yes: Cloudflare Workers, Vercel and Deno all provide the same `fetch`. From a browser page, no: the key would be visible to every visitor. Put a small server route in between that holds the key and forwards only the parameters you allow.

The same rule applies to mobile apps. Anything shipped to a user's device can be read, so keys stay on your servers.

## How do I keep costs predictable in Node.js?

Count the calls you make: one successful call is one credit, at $0.56/1K searches on the 100,000 searches pack. Check `credits_remaining` before a large batch and stop the pool when it gets close to zero rather than collecting a series of `402` answers. Prepaid search credits never expire: unused searches stay on your account until you use them, so an unused remainder is kept for the next job.

## Node.js questions

### Is there an npm package for SerpCrypto?

No. The client above is about 40 lines of standard JavaScript using `fetch`, so there is nothing of ours to install. Copy it into your project and adapt it.

### Does this work with CommonJS (require)?

The examples use ES modules for top-level `await`. In a CommonJS file, wrap the calls in an `async` function and use `fetch` the same way; it is global in Node.js 18+.

### How do I use it with TypeScript?

Copy the JSDoc shapes into an `interface SearchResponse` and type the result of `res.json()` with it. Optional blocks such as `organic_results?` must stay optional, because a page may not show them.

### Which Node.js version do I need?

Node.js 18 or later for the built-in `fetch`. `AbortSignal.timeout` is available from Node.js 17.3, so 18 covers everything on this page.

---

SerpCrypto: SerpCrypto is a SERP API for developers and AI agents that returns Google and Bing results as JSON, paid in crypto with no ID upload. Prepaid search credits from $0.30/1K searches never expire. No KYC — no ID, no selfie, no documents. Pay in crypto.
