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/v1

Authentication#

All API requests require a Bearer token in the Authorization header. API keys start with gk_live_ and are 48 characters long.

Example request
bash
curl https://geogrid.dev/api/v1/credits \
  -H "Authorization: Bearer gk_live_a1b2c3d4e5f6g7h8i9j0..."
Keep your API keys secret. Never expose them in client-side code, public repositories, or browser requests. If a key is compromised, revoke it immediately in Settings.

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.

ScopeGrants
scans.readList scans (GET /scans), read individual scan + grid_data (GET /scans/:id)
scans.writeLaunch new scans (POST /scans)
credits.readRead credits balance (GET /credits)
Legacy keys created before scopes existed have full access (all 3 scopes implicitly). New keys default to the full set if you don't pick scopes explicitly.

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.

PlanRequests / minPrice
FreeNo API access$0
StarterNo API access$19/mo
Freelance60$49/mo
Agency120$149/mo
Enterprise250$499/mo

Response headers on every request:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117

When 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.

402Payment Required
{
  "error": {
    "code": "insufficient_credits",
    "message": "Insufficient credits",
    "credits_available": 12,
    "credits_required": 169
  }
}
StatusCodeMeaning
400invalid_*Validation error (bad parameters)
401missing_api_keyNo Authorization header
401invalid_api_keyKey not found or malformed
401revoked_api_keyKey has been revoked
402insufficient_creditsNot enough credits for this operation
403plan_requiredPaid plan needed for API access
403insufficient_scopeAPI key lacks the required scope (e.g. scans.write)
403scan_limit_reachedPlan scan quota exceeded
403account_blockedAccount suspended
404not_foundResource doesn't exist
429rate_limit_exceededToo many requests
500internal_errorServer 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

ParameterTypeRequiredDescription
keywordstringRequiredSearch query (e.g. "plumber near me"). Max 200 chars.
latitudenumberRequiredCenter latitude (-90 to 90)
longitudenumberRequiredCenter longitude (-180 to 180)
target_cidstringOptionalGoogle Maps CID of the business to track. Either target_cid or target_name required.
target_namestringOptionalBusiness name (used for fuzzy matching if no CID)
grid_sizenumberOptionalGrid resolution: 5, 7, 13, or 21 (21 = Enterprise only). Default 13. Credits = grid_size²
radius_metersnumberOptionalScan radius in meters. 500-50000 (default: 5000)
diff_from_scan_idstring (UUID)OptionalBaseline 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.
hlstringOptionalGoogle search language code (default: "en"). Examples: "fr", "de", "ja".
glstringOptionalGoogle search country code (default: "us"). Examples: "fr", "gb", "de".
tagsstring[]OptionalTags for organization (max 10, 50 chars each)
Launch a scan
bash
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"]
  }'
202Accepted
{
  "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."
}
Async scans: POST /scans returns 202 Accepted with a pending status. Poll GET /scans/:id until status changes to completed, partial, or failed.
Visibility score is calculated as 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

ParameterTypeRequiredDescription
limitnumberOptionalResults per page, 1-100 (default: 20)
offsetnumberOptionalSkip N results (default: 0)
statusstringOptionalFilter: completed, partial, failed, running, pending
keywordstringOptionalFilter by keyword (partial match)
tagstringOptionalFilter by tag (exact match)
sortstringOptionalSort: newest (default), oldest, score
List completed scans
bash
curl "https://geogrid.dev/api/v1/scans?status=completed&limit=5&sort=newest" \
  -H "Authorization: Bearer gk_live_a1b2c3d4..."
200OK
{
  "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

Get scan details
bash
curl https://geogrid.dev/api/v1/scans/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer gk_live_a1b2c3d4..."
200OK
{
  "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"
  }
}
Each node in 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

Check balance
bash
curl https://geogrid.dev/api/v1/credits \
  -H "Authorization: Bearer gk_live_a1b2c3d4..."
200OK
{
  "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"
      }
    ]
  }
}
Credit costs: 5×5 = 25, 7×7 = 49, 13×13 = 169, 21×21 = 441 (Enterprise). Cache hits are free, so actual cost is usually 30-70% of max. Smart diff rescans (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

ParameterTypeRequiredDescription
namestringOptionalHuman-readable label (max 50 chars). Default: "Default".
scopesstring[]OptionalSubset of scans.read, scans.write, credits.read. If omitted, the key gets all three.
Create a read-only key (session auth only)
bash
curl -X POST https://geogrid.dev/api/v1/keys \
  -H "Content-Type: application/json" \
  -b "session_cookie=..." \
  -d '{ "name": "Client Reporting", "scopes": ["scans.read"] }'
201Created
{
  "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."
}
The full API key is only returned once at creation time. It is stored as a hashed digest and cannot be retrieved later. Maximum 5 active keys per account.

List API Keys#

List active keys
bash
curl https://geogrid.dev/api/v1/keys \
  -b "session_cookie=..."
200OK
{
  "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.

Revoke a key
bash
curl -X DELETE https://geogrid.dev/api/v1/keys/key_uuid \
  -b "session_cookie=..."
200OK
{
  "success": true,
  "id": "key_uuid"
}

Quickstart#

Get from zero to your first scan in 3 steps.

1

Get your API key

Go to Settings, scroll to "API Keys", create a key, and copy it.

2

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
  }'
3

Check your credits

curl https://geogrid.dev/api/v1/credits \
  -H "Authorization: Bearer $GEOGRID_API_KEY"
A 5x5 grid costs a maximum of 25 credits. With cache hits, your first scan might cost as few as 10-15 credits.

Pagination#

List endpoints use offset-based pagination. The response always includes a pagination object.

Page through results
javascript
// 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.

In addition to raw JSON Webhooks, Geogrid natively supports Slack and Discord webhooks. Simply paste your channel webhook URL in the Settings page and we'll send a formatted card when a scan completes.

Webhook Payload (application/json)

200POST to your endpoint
{
  "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"
  }
}