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
| 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 explains.
// 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 — 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 — 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.