The official Node.js and TypeScript client for GeoGridAPI, the geo-grid rank tracking API for Google Maps (local SEO).
Send a Google Business Profile and a keyword: GeoGridAPI measures the listing's rank in the Google Maps results at every point of a grid around it, and returns the rank matrix, the competitors ranking ahead, the change since the previous grid and a PNG heatmap. 1 credit = 1 grid point.
- Fully typed, zero dependencies, ESM and CommonJS
- Node.js 20+, Bun, Deno, Cloudflare Workers and edge runtimes (uses
fetchand Web Crypto) - Automatic retries with an idempotency key: a retried request never creates or charges a second grid
grids.run()starts a grid and waits for the result- Webhook signature verification
npm install geogridapiCreate an account on geogridapi.io (500 free credits), then an API key in the console. A gga_live_ key measures real grids and spends credits. A gga_test_ key returns free sample grids, handy while you build.
import { GeoGridAPI } from "geogridapi";
const gga = new GeoGridAPI(); // reads process.env.GEOGRIDAPI_API_KEY
const grid = await gga.grids.run({
query: "Joe's Pizza, New York", // or place_id: "ChIJ…"
keyword: "pizza",
size: 7, // 7 × 7 = 49 credits
radius_km: 3,
});
console.log(grid.summary);
// { average_rank: 4.3, average_rank_with_misses: 4.6, top3_points: 28, found_points: 48 }
console.log(grid.ranks); // 7 × 7 matrix, row 0 = north, null = not in the top 20
console.log(grid.competitors); // who ranks aheadA grid takes 20 seconds to 2 minutes. grids.run() polls until it is completed or failed (a failed grid is refunded). To start it without waiting, use grids.create() then grids.get(), or pass a webhook_url.
// Find a listing and its place_id (free)
const { data: places } = await gga.places.search("Joe's Pizza, New York");
// Exact cost before running (free)
const estimate = await gga.grids.estimate({ place_id: places[0].place_id, keyword: "pizza", shape: "circle", density: "medium" });
console.log(estimate.credits, estimate.balance_after);
// Start a grid without waiting
const pending = await gga.grids.create({ place_id: "ChIJ…", keyword: "pizza", webhook_url: "https://example.com/hooks/geogrid" });
const grid = await gga.grids.wait(pending.id);
// PNG heatmap of a completed grid (free)
import { writeFile } from "node:fs/promises";
await writeFile("map.png", await gga.grids.image(grid.id, { lang: "en" }));
// Ranking trend of the same map over time (free)
const history = await gga.history.get({ place_id: "ChIJ…", keyword: "pizza" });
console.log(history.change?.average_rank); // negative = better
// Up to 50 grids in one call: one listing × several keywords, or several listings × one keyword
const batch = await gga.batches.run({
size: 5,
items: [
{ place_id: "ChIJ…", keyword: "pizza" },
{ place_id: "ChIJ…", keyword: "pizza delivery" },
],
});
// Repeat a grid every week or every month
const schedule = await gga.schedules.create({ grid_id: grid.id, frequency: "weekly", weekday: 1 });
await gga.schedules.update(schedule.id, { status: "paused" });
// Every grid, page after page
for await (const g of gga.grids.listAll({ keyword: "pizza" })) console.log(g.id, g.summary?.average_rank);
// Credits left
const { points } = await gga.balance.get();| Parameter | Values |
|---|---|
shape |
square (default), circle, diamond, hexagon, cross, ellipse, rectangle |
size |
Square only: 3, 5, 7 (default) or 9 → 9, 25, 49 or 81 credits |
density |
Other shapes: low, medium (default) or high, 13 to 49 points |
radius_km |
0.5 to 30 km from center to edge (other shapes: from 1 km) |
language, country |
2-letter codes |
Points that fall at sea are neither measured nor charged.
Every API error is a GeoGridAPIError with status, code and message, and a subclass per status:
import { InsufficientCreditsError, NotFoundError, RateLimitError } from "geogridapi";
try {
await gga.grids.run({ query: "Unknown place", keyword: "pizza" });
} catch (error) {
if (error instanceof InsufficientCreditsError) console.log("Top up at https://geogridapi.io");
else if (error instanceof NotFoundError) console.log(error.code); // place_not_found
else throw error;
}| Class | Status |
|---|---|
InvalidRequestError |
400 |
AuthenticationError |
401 |
InsufficientCreditsError |
402 |
NotFoundError |
404 |
ConflictError |
409 (grid_not_ready, already_scheduled…) |
UnprocessableEntityError |
422 (schedule_limit) |
RateLimitError |
429 |
APIError |
5xx |
APIConnectionError |
network error or timeout |
WaitTimeoutError |
run()/wait() gave up; the grid keeps running |
Network errors, 429 and 5xx are retried twice with backoff (Retry-After is honored). Grid and batch creations send an Idempotency-Key, so these retries never create or charge a second grid. The key is new for each call: when your own code retries a creation (a queue job, a cron), pass your own idempotencyKey, for example an order or job id:
await gga.grids.run({ place_id: "ChIJ…", keyword: "pizza" }, { idempotencyKey: `weekly-report-${jobId}` });While run() or wait() waits, network errors, 429 and 5xx are waited out until the timeout. If waiting still fails, the error carries gridId (or batchId): the grid exists, read it later with grids.get() instead of running it again. A failed grid has status: "failed" and error: { code, message }.
Set maxRetries and timeoutMs on the client or per call.
When a grid ends, GeoGridAPI posts a grid.completed or grid.failed event to its webhook_url, signed with your whsec_ secret (console → Webhook). Verify it against the raw body:
import { constructEvent } from "geogridapi";
// Express: app.post("/hooks/geogrid", express.raw({ type: "application/json" }), handler)
async function handler(req, res) {
try {
const event = await constructEvent(req.body, req.get("GeoGridAPI-Signature"), process.env.GEOGRIDAPI_WEBHOOK_SECRET);
if (event.type === "grid.completed") console.log(event.data.summary);
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
}verifyWebhookSignature() returns a boolean instead. Signatures older than 5 minutes are refused (toleranceSeconds).
const gga = new GeoGridAPI({
apiKey: "gga_live_…", // default: process.env.GEOGRIDAPI_API_KEY
timeoutMs: 60_000, // per HTTP request
maxRetries: 2,
fetch: customFetch, // optional
});- MCP server for Claude, ChatGPT, Cursor and any MCP client:
https://api.geogridapi.io/mcp - Python:
pip install geogridapi, andpip install langchain-geogridapifor LangChain agents - No-code: n8n node, Apify Actor, Make, Zapier (guide)
- API reference · OpenAPI specification