# GeoGrid Rank Tracker API > GeoGrid Rank Tracker scans how a business ranks in Google local search results across a geographic grid. Submit a scan with a business name, keyword, and center coordinates, then retrieve an interactive Leaflet.js heatmap showing rank at each grid point. ## Base URL `https://YOUR_DOMAIN/dominator` All endpoints below are relative to this base URL. ## Discovery - `GET /dominator/llms.txt` — This documentation - `GET /dominator/.well-known/llms.txt` — Redirects to `/dominator/llms.txt` - `GET /dominator/robots.txt` — Robots file with `Llms-Txt` directive ## Authentication All API endpoints (except `/health`, `/scan/{scan_id}/map`, `/guide`, `/llms.txt`, `/robots.txt`) require an `X-API-Key` header. Browser pages (`/scans/view`, `/scan/new`, `/scan/{scan_id}/add`) use cookie-based session auth set via `POST /login`. API endpoints use the `X-API-Key` header. ``` X-API-Key: your_api_key_here ``` ## Endpoints ### GET /health Health check. No authentication required. **Response:** ```json {"status": "ok"} ``` --- ### POST /scan Create a new geogrid scan. The scan runs asynchronously in the background. **Headers:** - `X-API-Key` (required): Your API key - `Content-Type: application/json` **Request Body (JSON):** | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `business` | string | Yes | — | Business name to track. Can also be a Google Place ID (starts with "ChIJ"). | | `keyword` | string | Yes | — | Search keyword (e.g. "plumber", "dentist near me"). | | `lat` | float | Yes | — | Center latitude of the grid. | | `lng` | float | Yes | — | Center longitude of the grid. | | `grid_size` | int | No | 7 | Grid dimension. Must be one of: 3, 5, 7, 9, 13, 15. Creates an NxN grid (e.g. 7 = 49 points). | | `grid_spacing_m` | int | No | 3219 | Distance between grid points in meters. Default is ~2 miles (3219m). | | `scan_type` | string | No | "local_finder" | Search type: `"local_finder"`, `"maps"`, `"both"`, or `"organic"`. | **Example Request:** ```bash curl -X POST https://YOUR_DOMAIN/dominator/scan \ -H "X-API-Key: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "business": "Dzala General Contractor LLC", "keyword": "general contractor", "lat": 38.9186, "lng": -77.2311, "grid_size": 7, "grid_spacing_m": 3219, "scan_type": "local_finder" }' ``` **Response (202-style):** ```json { "scan_id": "sc_a1b2c3d4e5", "status": "processing", "points": 49, "estimated_seconds": 98, "poll_url": "/dominator/scan/sc_a1b2c3d4e5", "html_url": "/dominator/scan/sc_a1b2c3d4e5/map" } ``` **Scan Types Explained:** - `local_finder` — Google Local Finder results (default, recommended). Shows the local business listings that appear when users click "More businesses" on Google. - `maps` — Google Maps search results. - `both` — Runs both `maps` and `local_finder` at each point (2x the cost). Uses maps rank as the primary display rank. - `organic` — Standard organic search results, filtering out directory sites (Yelp, Facebook, etc.). **Automatic Organic Fallback:** If a non-organic scan (`local_finder`, `maps`, or `both`) fails to resolve the business name to a Google Place ID, the scan automatically falls back to `organic` mode instead of failing. The scan's `scan_type` is updated to `"organic"` and a `fallback_organic: true` flag is set on the scan record. In organic mode, the business is matched by fuzzy name and domain matching rather than Place ID. --- ### GET /scans List all scans for the authenticated API key, sorted newest first. Does not include the heavy `points` data — use `GET /scan/{scan_id}` for full results. **Headers:** - `X-API-Key` (required): Your API key **Query Parameters:** | Param | Type | Default | Description | |-------|------|---------|-------------| | `search` | string | "" | Filter by business name, keyword, or scan ID. | | `page` | int | 1 | Page number (1-indexed). | | `per_page` | int | 20 | Results per page (1-100). | **Response:** ```json { "scans": [ { "scan_id": "sc_a1b2c3d4e5", "status": "complete", "business": "Dzala General Contractor LLC", "keyword": "general contractor", "grid_size": 7, "scan_type": "local_finder", "created_at": "2026-02-09 01:59:31.750000", "summary": { "avg_rank": 4.76, "top3_pct": 0.35, "not_found_pct": 0.14 }, "html_url": "/dominator/scan/sc_a1b2c3d4e5/map" } ], "total": 42, "page": 1, "per_page": 20, "pages": 3 } ``` --- ### POST /scheduled-scans/ensure Idempotently creates or updates a monthly scheduled scan. Used by FlashCrafter after website publish. **Headers:** - `X-API-Key` (required) - `Content-Type: application/json` **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `external_business_id` | string | Yes | FlashCrafter Business ID. | | `external_site_id` | string | No | FlashCrafter Site ID. | | `schedule_key` | string | No | Stable idempotency key. Generated when omitted. | | `business` | string | Yes | Business name or Google Place ID. | | `keyword` | string | Yes | Keyword to scan monthly. | | `lat` | float | Yes | Grid center latitude. | | `lng` | float | Yes | Grid center longitude. | | `grid_size` | int | No | 3, 5, 7, 9, 13, or 15. Defaults to 7. | | `grid_spacing_m` | int | No | Defaults to 3219. | | `scan_type` | string | No | `local_finder`, `maps`, `both`, or `organic`. | | `cadence` | string | No | `monthly` only. | | `published_url` | string | No | Client website URL for display. | | `trigger_initial` | boolean | No | Defaults to true. Creates the baseline scan if no runs exist. | **Response:** Includes the schedule, whether it was newly created, whether an initial run was queued, and recent runs. --- ### GET /scheduled-scans List scheduled scans with recent run history. **Headers:** - `X-API-Key` (required) **Query Parameters:** - `external_business_id` - `external_site_id` --- ### GET /scheduled-scans/{schedule_id}/runs List run history for one scheduled scan. **Headers:** - `X-API-Key` (required) **Query Parameters:** - `limit` (1-100, default 24) --- ### POST /login Browser login. Sets a signed session cookie (`grid_session`) so the API key is no longer in the URL. **Form fields:** - `api_key` (required): Your API key On success, redirects (303) to `/scans/view`. On invalid key, re-renders the login form with an error message. --- ### GET /logout Clears the session cookie and redirects to `/scans/view` (which shows the login form). --- ### GET /scans/view Browser-friendly HTML page listing all scans. **Uses a session cookie for authentication** — no query-parameter API key needed. If the session is missing or invalid, shows a login form that POSTs to `/login`. **URL:** ``` https://YOUR_DOMAIN/dominator/scans/view ``` Displays a table with scan ID (linked to map), status, business, keyword, grid size, type, avg rank, top 3%, and creation date. --- ### GET /scan/{scan_id} Poll scan status or retrieve completed results. Scans are scoped to the API key that created them. **Headers:** - `X-API-Key` (required): Must match the key used to create the scan **Response (processing):** ```json { "scan_id": "sc_a1b2c3d4e5", "status": "processing", "progress": "12/49" } ``` **Response (complete):** ```json { "scan_id": "sc_a1b2c3d4e5", "status": "complete", "business": { "place_id": "ChIJb0nIRwazt4kRyBAuQXFQwns", "cid": "8917778659502067912", "name": "Dzala General Contractor LLC", "address": "1934 Old Gallows Rd Suite 350, Tysons, VA 22182" }, "keyword": "general contractor", "grid_size": 7, "html_url": "/dominator/scan/sc_a1b2c3d4e5/map", "summary": { "avg_rank": 4.76, "top3_pct": 0.35, "not_found_pct": 0.14 }, "points": [ { "row": 0, "col": 0, "lat": 38.8319, "lng": -77.3178, "rank": 3, "top_results": [ { "rank": 1, "name": "Some Contractor", "place_id": "ChIJ...", "cid": "123...", "rating": 4.8, "reviews": 125, "address": "123 Main St..." } ] } ] } ``` **Response (failed):** ```json { "scan_id": "sc_a1b2c3d4e5", "status": "failed", "error": "Business not found: Invalid Business Name" } ``` --- ### GET /scan/{scan_id}/map Interactive HTML map visualization. **No authentication required** — this URL is shareable. - While processing: shows a spinner with progress bar (auto-refreshes every 5 seconds) - On completion: renders a full Leaflet.js map with color-coded rank markers - On failure: shows error message **Rank Color Legend:** - Green: Rank 1-3 - Yellow: Rank 4-10 - Red: Rank 11-20 - Gray: Not found in top 20 Click any grid point on the map to see the full top-20 rankings in the right panel. The map page includes a **"+ Add"** button that lets authenticated users append another grid of points to the same scan (see `POST /scan/{scan_id}/add` below). --- ### POST /scan/{scan_id}/add Append a new grid to an existing completed scan. Creates a "child" scan that is processed in the background. When the child completes, its points are automatically merged into the parent scan and summary stats are recomputed. Child scans do not appear in the `/scans` list. **Authentication:** Session cookie (browser). Must be logged in as the scan's owner. **Request Body (JSON):** | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `lat` | float | Yes | — | Center latitude for the new grid. | | `lng` | float | Yes | — | Center longitude for the new grid. | | `grid_size` | int | No | parent's value | Grid dimension (3, 5, 7, 9, 13, or 15). Defaults to the parent scan's grid size. | | `grid_spacing_m` | int | No | parent's value | Distance between grid points in meters. Defaults to the parent scan's spacing. | | `scan_type` | string | No | parent's value | `"local_finder"`, `"maps"`, `"both"`, or `"organic"`. Defaults to the parent scan's type. | **Response:** ```json { "child_scan_id": "sc_x1y2z3a4b5", "status": "pending" } ``` Poll the child scan status via `GET /scan/{child_scan_id}` with `X-API-Key` header. When `status` is `"complete"`, the child's points have been merged into the parent. **Validation:** - Parent scan must exist, belong to the authenticated user, and have `status: "complete"`. - Returns 400 if parent is not complete, 404 if not found or not owned. --- ### GET /scan/new Browser-friendly form to create a new scan. **Requires session auth.** Supports query parameter pre-fills (`business`, `keyword`, `grid_size`, `grid_spacing_m`, `scan_type`) for the "Add" flow from the scans list. --- ### GET /guide User training guide page. **No authentication required.** Explains scan types, grid settings, New vs. Add workflows, map reading, and cost estimates. Includes a link to the full Google Docs training guide. --- ## Workflows ### New scan 1. **Submit scan** via `POST /dominator/scan` — returns `scan_id` 2. **Poll status** via `GET /dominator/scan/{scan_id}` — check `status` field until `"complete"` or `"failed"` 3. **View results** via `GET /dominator/scan/{scan_id}/map` in a browser, or parse the JSON from step 2 4. **List all scans** via `GET /dominator/scans` — see history of all your scans ### Append to existing scan 1. **Add grid** via `POST /dominator/scan/{scan_id}/add` with new center coordinates — returns `child_scan_id` 2. **Poll child** via `GET /dominator/scan/{child_scan_id}` — same polling as above 3. When complete, child points are **automatically merged** into the parent scan 4. Reload the parent's map page to see all points, or use the live map which injects markers in real-time Typical processing time: 60-120 seconds for a 7x7 grid (49 points). ## Error Codes | Code | Description | |------|-------------| | 400 | Invalid request body (missing fields, invalid grid_size, invalid scan_type) | | 401 | Invalid or missing API key | | 404 | Scan not found (or belongs to different API key) | ## Summary Fields | Field | Description | |-------|-------------| | `avg_rank` | Average rank across all grid points where the business was found. `null` if never found. | | `top3_pct` | Fraction of total grid points where business ranked 1-3 (0.0 to 1.0). | | `not_found_pct` | Fraction of total grid points where business was not in top 20 (0.0 to 1.0). | ## Cost Uses DataForSEO Standard mode (normal priority). Cost per grid point: ~$0.0012. A 7x7 grid (49 points) costs ~$0.06. Using `scan_type: "both"` doubles the cost.