Skip to content

Cookbook: Automate SERP monitoring with the API

A complete Node.js script that runs localized proxy searches for a set of queries, waits for results, and writes them to JSON — ready to run on a schedule.

Goal: run a set of localized Google searches on a cadence (say, nightly), capture the parsed results, and write them somewhere you can diff or feed into a report — with no manual clicking.

  • An organization with the Search Engine Proxy feature and token balance.
  • An API key with the Search permission.
  • Node.js 18+ (for the built-in fetch), or adapt the logic to any language.

A proxy search is asynchronous: you POST the search, get back an id with status: "pending", then poll GET …/{id} until it’s completed. Each requested page costs one token. The script below wraps that create-then-poll loop.

Save as monitor.mjs:

const BASE_URL = process.env.ISEARCHFROM_BASE_URL ?? 'https://api.isearchfrom.com/v1';
const API_KEY = process.env.ISEARCHFROM_API_KEY;
if (!API_KEY) throw new Error('Set ISEARCHFROM_API_KEY');
// The searches to run each cycle. Add or edit freely.
const QUERIES = [
{ query: 'best crm software', country: 'US', language: 'en' },
{ query: 'crm für startups', country: 'DE', language: 'de' },
{ query: 'melhor crm', country: 'BR', language: 'pt' }
];
async function api(path, init = {}) {
const res = await fetch(`${BASE_URL}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
...(init.headers ?? {})
}
});
const body = await res.json().catch(() => ({}));
if (!res.ok) {
// body has { statusCode, code, message } — branch on `code`, log `message`.
throw new Error(`API ${res.status} (code ${body.code}): ${body.message}`);
}
return body;
}
async function runSearch({ query, country, language }) {
// 1. Create the search (1 page = 1 token).
const created = await api('/tools/serp/proxy-search', {
method: 'POST',
body: JSON.stringify({
query,
country,
language,
engine: 'google',
device: 'desktop',
pages: [1]
})
});
// 2. Poll until completed / failed / timeout.
const deadline = Date.now() + 90_000;
let search = created;
while (search.status === 'pending' || search.status === 'running') {
if (Date.now() > deadline) throw new Error(`Timed out: ${query}`);
await new Promise((r) => setTimeout(r, 2000));
search = await api(`/tools/serp/proxy-search/${created.id}`);
}
return search;
}
const results = [];
for (const q of QUERIES) {
try {
const search = await runSearch(q);
results.push({ ...q, status: search.status, search });
console.error(`✓ ${q.query} (${q.country}) → ${search.status}`);
} catch (err) {
results.push({ ...q, status: 'error', error: String(err) });
console.error(`✗ ${q.query} (${q.country}) → ${err.message}`);
}
}
// 3. Emit JSON on stdout so you can redirect it to a file or a pipeline.
process.stdout.write(JSON.stringify({ ranAt: new Date().toISOString(), results }, null, 2));

Run it and write today’s snapshot to a file:

Terminal window
export ISEARCHFROM_API_KEY="isk_your_key_here"
node monitor.mjs > "serp-$(date +%F).json"

cron (Linux/macOS) — nightly at 02:00:

0 2 * * * cd /path/to/monitor && ISEARCHFROM_API_KEY=isk_... /usr/bin/node monitor.mjs > "serp-$(date +\%F).json" 2>> monitor.log

GitHub Actions — a scheduled workflow (store the key as a repository secret):

name: serp-monitor
on:
schedule:
- cron: '0 2 * * *'
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: node monitor.mjs > snapshot.json
env:
ISEARCHFROM_API_KEY: ${{ secrets.ISEARCHFROM_API_KEY }}
- uses: actions/upload-artifact@v4
with: { name: serp-snapshot, path: snapshot.json }
  • Each completed entry has search.status === "completed" and pages with parsed organic results and ads.
  • Failed queries carry an error with the API code and message — handle those (bad country, insufficient balance, rate limit) rather than blindly retrying. See Errors, pagination & rate limits.
  • Diff over time — commit each snapshot and diff the JSON to spot ranking or ad changes.
  • More pages — set pages: [1, 2, 3] to capture deeper results (costs one token per page).
  • Let an agent do it — the same flow is available to an MCP agent via run_proxy_search + get_proxy_search, with no script to maintain.
  • Respect the rate limit — the default is 600 requests/minute; for large query sets add a small delay between searches.