GeoScored API

Integrate GEO audits into your workflow. Create scans, retrieve reports, and monitor AI visibility programmatically.

1. Getting started

The GeoScored API is a REST API that returns JSON. Every request must include an API key header. You can generate keys in Account Settings → API Keys.

Base URL

https://app.geoscored.ai/api/v1

Content type

application/json

The interactive OpenAPI specification is available at /api/docs (Swagger UI) and /api/redoc (ReDoc). This page covers the concepts and patterns that the auto-generated docs do not.

2. Authentication

Every request requires an API key passed in the X-API-Key header. Keys are scoped to your organization. All members of an org share the same scan history and credit balance.

# Pass your API key on every request
curl https://app.geoscored.ai/api/v1/scans \
  -H "X-API-Key: geo_your_key_here" \
  -H "Content-Type: application/json"

Key format

All keys begin with geo_. Requests with missing or malformed keys return 401.

Sandbox mode

Keys prefixed with geo_test_ activate sandbox mode. Sandbox scans do not consume credits and return synthetic data.

Org-scoped

API keys are scoped to your organization. Scans created by one key are visible to all keys in the same org.

3. Scan lifecycle

Scans are asynchronous. Creating a scan returns immediately with status pending. The engine processes the scan in the background. Poll the scan endpoint or use a webhook callback to detect completion.

Polling pattern

Poll GET /api/v1/scans/{'{'}scan_id{'}'} until status is COMPLETED or FAILED. Most scans complete within 60–120 seconds. Use an exponential backoff starting at 5 seconds.

Webhook alternative

Pass a callback_url when creating a scan to receive a POST notification when the scan completes. See the Webhooks section for payload details.

4. Endpoints

All endpoints require the X-API-Key header.
Method Path Description
POST /api/v1/scans Create a scan. Debits one credit.
GET /api/v1/scans List scans. Paginated, filterable.
GET /api/v1/scans/compare Compare two scans with computed deltas.
GET /api/v1/scans/{'{scan_id}'} Get a scan by ID (status + scores).
GET /api/v1/scans/{'{scan_id}'}/report Full audit report with all check results.
GET /api/v1/scans/{'{scan_id}'}/checks List all check results for a scan.
GET /api/v1/scans/{'{scan_id}'}/checks/{'{check_id}'} Get a single check result.
POST /api/v1/scans/{'{scan_id}'}/share Create a shareable report link.
PATCH /api/v1/scans/{'{scan_id}'} Update scan metadata (project, tracked URL).
DELETE /api/v1/scans/{'{scan_id}'} Delete a scan and all its records.
GET /api/v1/scans/export Export scan results as CSV.
GET /api/v1/scans/{'{scan_id}'}/pdf Download the PDF report for a completed scan.
POST /api/v1/scans Requires credit

Create a scan

Submits a URL for a GEO audit. The scan starts asynchronously. Returns 201 with the scan object in pending status. One credit is debited from your organization's balance at creation time.

Request body

Field Type Required Description
url string Yes The URL to audit. Must be publicly accessible HTTPS.
brand_name string No Brand name for AI mention checks. Defaults to the domain name.
callback_url string No HTTPS URL to POST scan results to when complete. Max 2048 chars.

Example request

curl -X POST https://app.geoscored.ai/api/v1/scans \
  -H "X-API-Key: geo_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "brand_name": "Example Corp",
    "callback_url": "https://your-server.com/webhooks/geoscored"
  }'

Response

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "url": "https://example.com",
  "brand_name": "Example Corp",
  "status": "PENDING",
  "overall_score": null,
  "grade": null,
  "grade_descriptor": null,
  "pillar_scores": null,
  "error_message": null,
  "created_at": "2026-03-01T10:00:00",
  "updated_at": "2026-03-01T10:00:00",
  "callback_url": "https://your-server.com/webhooks/geoscored"
}
GET /api/v1/scans

List scans

Returns a paginated list of scans for your organization. Filter by status or URL.

Query parameters

Parameter Type Default Description
limit integer 50 Results per page. Max 100.
offset integer 0 Number of results to skip.
status string all Filter: PENDING, RUNNING, COMPLETED, FAILED.
url string Exact URL match (normalized). Returns scan history for that URL.
tracked_url_id string Filter by tracked URL ID for timeline queries.
{
  "data": [/* array of scan objects */],
  "total": 42,
  "limit": 50,
  "offset": 0,
  "has_more": false
}
GET /api/v1/scans/{'{scan_id}'}/report

Get audit report

Returns the complete audit report for a completed scan: overall score, grade, pillar scores, all check results, and AI-generated summaries. Only available once the scan status is COMPLETED.

Query parameters

Parameter Type Default Description
audience string marketing marketing or engineering. Switches explanatory text in why_it_matters, grade_descriptor, and pillar descriptions. Scores are identical for both.
{
  "scan_id": "a1b2c3d4-...",
  "url": "https://example.com",
  "overall_score": 0.72,
  "overall_grade": "B",
  "grade_descriptor": "Good AI visibility with room to improve",
  "pillar_scores": {
    "geo": 0.68,
    "seo": 0.81
  },
  "total_checks": 28,
  "passing_checks": 19,
  "warning_checks": 6,
  "failing_checks": 3,
  "checks": [/* array of check result objects */],
  "geo_marketing_summary": "AI-generated summary...",
  "access_level": "full",
  "is_lite": false,
  "audience": "marketing"
}
GET /api/v1/scans/compare

Compare two scans

Returns a structured comparison of two scans with computed deltas for overall score, each pillar, and each individual check. Both scans must belong to your organization. Useful for tracking improvement over time.

Query parameters

Parameter Type Required Description
scan_a string Yes Scan ID for the baseline (earlier) scan.
scan_b string Yes Scan ID for the comparison (later) scan.
curl "https://app.geoscored.ai/api/v1/scans/compare?scan_a=ID_A&scan_b=ID_B" \
  -H "X-API-Key: geo_your_key"
POST /api/v1/scans/{'{scan_id}'}/share

Create a share token

Generates a 128-bit URL-safe share token. The token can be used to access the report at /reports/share/{'{token}'} without authentication. Tokens expire after 30 days and deactivate after 100 views. Maximum 10 tokens per scan per hour.

{
  "id": "share-uuid",
  "scan_id": "scan-uuid",
  "token": "AbCdEfGhIjKlMnOpQrSt_A",
  "share_url": "https://geoscored.ai/reports/share/AbCdEfGhIjKlMnOpQrSt_A",
  "expires_at": "2026-04-01T10:00:00",
  "view_limit": 100,
  "view_count": 0,
  "access_level": "full"
}

5. Error handling

All errors return a structured JSON envelope with a consistent shape. Rather than inspecting HTTP status codes alone, always read the error.code field for machine-readable error classification.

{
  "error": {
    "type": "invalid_request",
    "code": "invalid_url",
    "message": "The URL must be a publicly accessible HTTPS address.",
    "param": "url"
  }
}

Common error codes

HTTP code Meaning
401 api_key_required No X-API-Key header present.
401 api_key_invalid Key does not exist, is revoked, or has wrong format.
402 insufficient_credits No scan credits remaining. Purchase more to continue.
404 scan_not_found Scan does not exist or belongs to a different org.
400 scan_not_complete Report or checks requested before scan has finished.
422 invalid_url URL is not a valid public HTTPS address.
422 invalid_callback_url Callback URL must be HTTPS and publicly accessible.
429 org_rate_limit_exceeded Organization-level rate limit reached. Back off and retry.
500 internal_error Unexpected server error. Retrying usually resolves transient failures.

6. Rate limits

The API enforces two layers of rate limiting: IP-level limits applied before authentication, and organization-level limits applied after authentication. Organization limits are configurable per org and may differ from the defaults shown here.

Limit group Applies to Headers
api_write POST /api/v1/scans X-RateLimit-*
api_read All other GET endpoints X-RateLimit-*

Rate limit information is returned in response headers:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1740823200

429 responses include a Retry-After header with the number of seconds to wait before retrying. Do not retry immediately on 429. Back off exponentially.

7. Webhooks

Pass a callback_url when creating a scan to receive a POST request when the scan completes. The callback URL must be HTTPS and publicly reachable from the internet.

Callback payload

When a scan reaches a terminal state (COMPLETED or FAILED), GeoScored sends a POST request to your callback URL with the scan object as the body.

# POST https://your-server.com/webhooks/geoscored
# Content-Type: application/json
{
  "id": "a1b2c3d4-...",
  "url": "https://example.com",
  "status": "COMPLETED",
  "overall_score": 0.72,
  "grade": "B",
  "pillar_scores": {
    "geo": 0.68,
    "seo": 0.81
  },
  "created_at": "2026-03-01T10:00:00",
  "updated_at": "2026-03-01T10:01:45"
}
Retry behavior: If your endpoint returns a non-2xx response, GeoScored retries up to 3 times with exponential backoff (5s, 25s, 125s). Your endpoint should respond within 10 seconds to avoid timeout.

8. Python examples

These examples use httpx, a modern HTTP client for Python. Install it with pip install httpx.

Create a scan and poll for completion

import time
import httpx

API_KEY = "geo_your_key_here"
BASE_URL = "https://app.geoscored.ai/api/v1"

headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# 1. Create a scan
response = httpx.post(
    f"{BASE_URL}/scans",
    headers=headers,
    json={
        "url": "https://example.com",
        "brand_name": "Example Corp",
    },
)
response.raise_for_status()
scan = response.json()
scan_id = scan["id"]
print(f"Created scan {scan_id}, status: {scan['status']}")

# 2. Poll until terminal state
delay = 5
while scan["status"] not in ("COMPLETED", "FAILED"):
    time.sleep(delay)
    delay = min(delay * 2, 60)  # cap at 60s
    response = httpx.get(
        f"{BASE_URL}/scans/{scan_id}",
        headers=headers,
    )
    response.raise_for_status()
    scan = response.json()
    print(f"Status: {scan['status']}")

if scan["status"] == "FAILED":
    raise RuntimeError(f"Scan failed: {scan.get('error_message')}")

print(f"Score: {scan['overall_score']}, Grade: {scan['grade']}")

Retrieve the audit report

# 3. Retrieve audit report (after scan completes)
response = httpx.get(
    f"{BASE_URL}/scans/{scan_id}/report",
    headers=headers,
    params={"audience": "engineering"},  # or "marketing"
)
response.raise_for_status()
report = response.json()

print(f"Overall: {report['overall_score']:.1%} ({report['overall_grade']})")
print(f"Checks: {report['passing_checks']} pass, {report['failing_checks']} fail")

# List failing checks
for check in report["checks"]:
    if check["severity"] == "FAIL":
        print(f"  FAIL: {check['check_name']} - {check['summary']}")

Compare two scans

# 4. Compare scans to track improvement
response = httpx.get(
    f"{BASE_URL}/scans/compare",
    headers=headers,
    params={"scan_a": "older_scan_id", "scan_b": "newer_scan_id"},
)
response.raise_for_status()
comparison = response.json()["data"]

overall_delta = comparison["deltas"]["overall_score"]
sign = "+" if overall_delta >= 0 else ""
print(f"Overall score change: {sign}{overall_delta:.1%}")

for pillar in comparison["deltas"]["pillars"]:
    d = pillar["delta"]
    sign = "+" if d >= 0 else ""
    print(f"  {pillar['pillar_key']}: {sign}{d:.1%}")