Introduction#
The Geogrid API lets you programmatically launch rank tracking scans, retrieve results, and manage your credits. Every endpoint returns JSON and uses standard HTTP status codes.
Launch scans
Run grid scans via API
Real-time results
Get heatmap data instantly
Secure
SHA-256 hashed API keys
Base URL
https://geogrid.dev/api/v1Authentication#
All API requests require a Bearer token in the Authorization header. API keys start with gk_live_ and are 48 characters long.
curl https://geogrid.dev/api/v1/credits \
-H "Authorization: Bearer gk_live_a1b2c3d4e5f6g7h8i9j0..."Generate API keys from Dashboard → Settings → API Keys. API access requires a paid plan (Freelance, Agency, or Enterprise).
Scopes
Each API key carries a list of scopes that limit what it can do. Pick the smallest set that fits your use case — give read-only keys to clients, write-enabled keys to your automation, and rotate them independently.
| Scope | Grants |
|---|---|
| scans.read | List scans (GET /scans), read individual scan + grid_data (GET /scans/:id) |
| scans.write | Launch new scans (POST /scans) |
| credits.read | Read credits balance (GET /credits) |
A request with insufficient scope returns 403 Forbidden with a JSON body:
{
"error": "Insufficient scope",
"required": "scans.write",
"granted": ["scans.read"]
}Rate Limits#
Rate limits are enforced per API key, based on your plan. Every response includes rate limit headers.
| Plan | Requests / min | Price |
|---|---|---|
| Free | No API access | $0 |
| Starter | No API access | $19/mo |
| Freelance | 60 | $49/mo |
| Agency | 120 | $149/mo |
| Enterprise | 250 | $499/mo |
Response headers on every request:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117When rate limited, you'll receive a 429 response with a Retry-After: 60 header.
Error Handling#
Errors return a structured JSON object with a code and message. Some errors include additional context.
{
"error": {
"code": "insufficient_credits",
"message": "Insufficient credits",
"credits_available": 12,
"credits_required": 169
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_* | Validation error (bad parameters) |
| 401 | missing_api_key | No Authorization header |
| 401 | invalid_api_key | Key not found or malformed |
| 401 | revoked_api_key | Key has been revoked |
| 402 | insufficient_credits | Not enough credits for this operation |
| 403 | plan_required | Paid plan needed for API access |
| 403 | insufficient_scope | API key lacks the required scope (e.g. scans.write) |
| 403 | scan_limit_reached | Plan scan quota exceeded |
| 403 | account_blocked | Account suspended |
| 404 | not_found | Resource doesn't exist |
| 429 | rate_limit_exceeded | Too many requests |
| 500 | internal_error | Server error (contact support) |
POST /scans#
Launch a new rank tracking scan. The job is queued; poll GET /api/v1/scans/:id until status is completed. One credit is consumed per API call to Google (cache hits are free).
Required scope: scans.write
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| keyword | string | Required | Search query (e.g. "plumber near me"). Max 200 chars. |
| latitude | number | Required | Center latitude (-90 to 90) |
| longitude | number | Required | Center longitude (-180 to 180) |
| target_cid | string | Optional | Google Maps CID of the business to track. Either target_cid or target_name required. |
| target_name | string | Optional | Business name (used for fuzzy matching if no CID) |
| grid_size | number | Optional | Grid resolution: 5, 7, 13, or 21 (21 = Enterprise only). Default 13. Credits = grid_size² |
| radius_meters | number | Optional | Scan radius in meters. 500-50000 (default: 5000) |
| diff_from_scan_id | string (UUID) | Optional | Baseline scan ID for smart diff rescan. Must share the same keyword, center, radius, and grid_size. Only volatile nodes are re-sampled — credits charged accordingly. |
| hl | string | Optional | Google search language code (default: "en"). Examples: "fr", "de", "ja". |
| gl | string | Optional | Google search country code (default: "us"). Examples: "fr", "gb", "de". |
| tags | string[] | Optional | Tags for organization (max 10, 50 chars each) |
curl -X POST https://geogrid.dev/api/v1/scans \
-H "Authorization: Bearer gk_live_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"keyword": "plumber near me",
"target_name": "John Plumbing LLC",
"latitude": 48.8566,
"longitude": 2.3522,
"grid_size": 7,
"radius_meters": 3000,
"hl": "fr",
"gl": "fr",
"tags": ["paris", "q1-2026"]
}'{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "pending",
"keyword": "plumber near me",
"latitude": 48.8566,
"longitude": 2.3522,
"radius_meters": 3000,
"grid_size": 7,
"created_at": "2026-03-19T14:30:00.000Z",
"message": "Scan queued. Poll GET /api/v1/scans/:id for results."
}202 Accepted with a pending status. Poll GET /scans/:id until status changes to completed, partial, or failed.sum(1/rank) / totalLandNodes. Nodes where the business wasn't found (rank: null) and water nodes are excluded. A score closer to 1.0 means higher dominance.GET /scans#
List your scans with pagination and filters. Returns scan metadata without grid data (use GET /scans/:id for full data).
Required scope: scans.read
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| limit | number | Optional | Results per page, 1-100 (default: 20) |
| offset | number | Optional | Skip N results (default: 0) |
| status | string | Optional | Filter: completed, partial, failed, running, pending |
| keyword | string | Optional | Filter by keyword (partial match) |
| tag | string | Optional | Filter by tag (exact match) |
| sort | string | Optional | Sort: newest (default), oldest, score |
curl "https://geogrid.dev/api/v1/scans?status=completed&limit=5&sort=newest" \
-H "Authorization: Bearer gk_live_a1b2c3d4..."{
"data": [
{
"id": "a1b2c3d4-...",
"keyword": "plumber near me",
"target_name": "John Plumbing LLC",
"latitude": 48.8566,
"longitude": 2.3522,
"radius_meters": 3000,
"grid_size": 7,
"status": "completed",
"nodes_sampled": 49,
"credits_consumed": 34,
"visibility_score": 4.2,
"tags": ["paris"],
"created_at": "2026-03-19T14:30:00.000Z",
"completed_at": "2026-03-19T14:30:12.500Z"
}
],
"pagination": {
"total": 42,
"limit": 5,
"offset": 0,
"has_more": true
}
}GET /scans/:id#
Retrieve a single scan with the complete grid_data array. This is the data you need to render a heatmap.
Required scope: scans.read
curl https://geogrid.dev/api/v1/scans/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer gk_live_a1b2c3d4..."{
"data": {
"id": "a1b2c3d4-...",
"keyword": "plumber near me",
"target_cid": "0x12345",
"target_name": "John Plumbing LLC",
"latitude": 48.8566,
"longitude": 2.3522,
"radius_meters": 3000,
"grid_size": 7,
"status": "completed",
"nodes_sampled": 49,
"credits_consumed": 34,
"visibility_score": 0.42,
"grid_data": [
{
"lat": 48.8612,
"lng": 2.3450,
"rank": 1,
"water": false,
"competitors": [
{ "name": "Plombier Express", "rank": 2 },
{ "name": "AquaFix", "rank": 3 },
{ "name": "SOS Plomberie", "rank": 4 },
{ "name": "LeakPro", "rank": 5 },
{ "name": "TuyauNet", "rank": 6 }
]
},
{
"lat": 48.8612,
"lng": 2.3498,
"rank": 3,
"water": false,
"competitors": []
},
{
"lat": 48.8500,
"lng": 2.3400,
"rank": null,
"water": true,
"competitors": []
}
],
"tags": ["paris"],
"notes": "Baseline scan before campaign",
"error_message": null,
"created_at": "2026-03-19T14:30:00.000Z",
"completed_at": "2026-03-19T14:30:12.500Z"
}
}grid_data includes water (boolean — true if the coordinate is over water, excluded from scoring) and competitors (array of the top 5 businesses at that grid point with their rank).GET /credits#
Get your current credit balance, plan details, and recent transactions.
Required scope: credits.read
curl https://geogrid.dev/api/v1/credits \
-H "Authorization: Bearer gk_live_a1b2c3d4..."{
"data": {
"credits": 8456,
"plan": "agency",
"plan_label": "Agency",
"credits_monthly": 20000,
"plan_renews_at": "2026-04-19T00:00:00.000Z",
"recent_transactions": [
{
"id": "tx_001",
"amount": -34,
"reason": "scan_consumed",
"metadata": { "scan_id": "a1b2c3d4-..." },
"created_at": "2026-03-19T14:30:12.500Z"
},
{
"id": "tx_002",
"amount": 20000,
"reason": "subscription",
"metadata": {},
"created_at": "2026-03-19T00:00:00.000Z"
}
]
}
}diff_from_scan_id) only charge for volatile nodes — typically 10-30% of the grid.Create API Key#
API keys are managed from the dashboard UI at Settings → API Keys. You can also create them programmatically (requires session auth, not API key).
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Optional | Human-readable label (max 50 chars). Default: "Default". |
| scopes | string[] | Optional | Subset of scans.read, scans.write, credits.read. If omitted, the key gets all three. |
curl -X POST https://geogrid.dev/api/v1/keys \
-H "Content-Type: application/json" \
-b "session_cookie=..." \
-d '{ "name": "Client Reporting", "scopes": ["scans.read"] }'{
"data": {
"id": "key_uuid",
"name": "Client Reporting",
"key_prefix": "gk_live_a1b2...",
"scopes": ["scans.read"],
"key": "gk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8",
"created_at": "2026-03-19T15:00:00.000Z"
},
"warning": "Store this key securely. It will not be shown again."
}List API Keys#
curl https://geogrid.dev/api/v1/keys \
-b "session_cookie=..."{
"data": [
{
"id": "key_uuid",
"name": "Production",
"key_prefix": "gk_live_a1b2...",
"scopes": ["scans.read", "scans.write", "credits.read"],
"last_used_at": "2026-03-19T14:55:00.000Z",
"requests_count": 1247,
"created_at": "2026-03-01T10:00:00.000Z"
}
]
}Revoke API Key#
Revoking a key is immediate and permanent. Any request using the revoked key will return 401.
curl -X DELETE https://geogrid.dev/api/v1/keys/key_uuid \
-b "session_cookie=..."{
"success": true,
"id": "key_uuid"
}Quickstart#
Get from zero to your first scan in 3 steps.
Get your API key
Go to Settings, scroll to "API Keys", create a key, and copy it.
Launch your first scan
export GEOGRID_API_KEY="gk_live_your_key_here"
curl -X POST https://geogrid.dev/api/v1/scans \
-H "Authorization: Bearer $GEOGRID_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"keyword": "pizza delivery",
"target_name": "Mario Pizza",
"latitude": 40.7128,
"longitude": -74.0060,
"grid_size": 5,
"radius_meters": 2000
}'Check your credits
curl https://geogrid.dev/api/v1/credits \
-H "Authorization: Bearer $GEOGRID_API_KEY"Pagination#
List endpoints use offset-based pagination. The response always includes a pagination object.
// Node.js example
const API = "https://geogrid.dev/api/v1";
const KEY = "gk_live_your_key_here";
async function getAllScans() {
let allScans = [];
let offset = 0;
const limit = 100;
while (true) {
const res = await fetch(
`${API}/scans?limit=${limit}&offset=${offset}`,
{ headers: { Authorization: `Bearer ${KEY}` } }
);
const { data, pagination } = await res.json();
allScans.push(...data);
if (!pagination.has_more) break;
offset += limit;
}
return allScans; // all your scans
}Webhooks#
Receive real-time HTTP POST notifications when your scans finish successfully or partially. Configure webhooks in Settings.
Webhook Payload (application/json)
{
"event": "scan.completed",
"timestamp": "2026-03-19T14:30:12.500Z",
"scan": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"keyword": "plumber near me",
"target_name": "John Plumbing LLC",
"visibility_score": 0.42,
"status": "completed",
"grid_size": 7,
"nodes_sampled": 49,
"credits_consumed": 34,
"url": "https://geogrid.dev/dashboard/scans/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Geogrid API v1