Quickstart Guide

Your first API call in minutes

This guide takes you through the real geog.ai journey: sign up, grab an API key, make a first deterministic call to resolve spatial context, run a physics simulation with job polling, and — separately — let the AI Spatial Analyst plan an analysis for you. Every request, response, and endpoint below matches the live API.

What you'll build

The signup → key → install → first call journey is four steps. Steps 1–3 are pure deterministic physics; step 4 is an optional, clearly-separated AI layer on top of the same engine.

Every call is authenticated with a Bearer key you create in your dashboard. The base URL is https://api.geog.ai/v1.

Estimated time: 5–10 minutes. Steps 2–4 are shown three ways each — raw HTTP with cURL, the Python SDK (pip install geog-ai), and the TypeScript SDK (npm i @geog-ai/sdk). Pick whichever you prefer.
Prefer an SDK? Install the official client for your language — pip install geog-ai (Python) or npm i @geog-ai/sdk (TypeScript). Each step below shows the SDK call alongside the raw request. See the Language SDKs page for full install notes.
1
Sign up & create an API key
~1 min

Create your account at geog.ai/signup. Signup provisions a tenant on the free tier and issues your first API key automatically — it's shown once on the confirmation screen, so copy it right away.

Need more keys, or want to rotate the first one? Manage all your keys from your dashboard at geog.ai/account — create, rename, rotate, and revoke. Each new key's secret is displayed exactly once at creation time.

Your API key format TEXT
MFRGGZDFMZTWQ.QK7X2P9M4RVT8N6WHYD3ZCBJ5FLA0EGU

A key is two dot-separated halves: a short public prefix (identifies the key) and a high-entropy secret (shown only once — geog.ai stores only its hash). Send the whole string as a Bearer token: Authorization: Bearer <prefix>.<secret>.

Keys carry scopes that gate what they can do: read (context, nearby, jurisdiction, AI planning), simulate (submit propagation jobs), and optimize. Your first key is issued with all three so you can complete this quickstart end to end.

Store your key as an environment variable so it never appears in source code:

# Add to ~/.zshrc or ~/.bashrc export GEOG_API_KEY="MFRGGZDFMZTWQ.QK7X2P9M4RVT8N6WHYD3ZCBJ5FLA0EGU"
# pip install geog-ai import os from geog_ai import GeogClient # Reads Authorization: Bearer $GEOG_API_KEY for you geog = GeogClient(api_key=os.environ["GEOG_API_KEY"])
// npm i @geog-ai/sdk (Node ≥ 18) import { GeogClient } from "@geog-ai/sdk"; // Sends Authorization: Bearer $GEOG_API_KEY for you const geog = new GeogClient({ apiKey: process.env.GEOG_API_KEY! });
Never commit your API key to source control. Use environment variables, a .env file (excluded from git), or a secrets manager. A leaked key can be revoked instantly from your dashboard.
2
Your first call: resolve context with GET /context
~2 min

Your first successful call is a deterministic one — no AI, no async job, just a direct read. GET /v1/context takes a registered device_id and returns a fully resolved SpatialState envelope: location, terrain, meteorology, nearby entities, jurisdiction, infrastructure, plus a confidence score and data provenance.

Two query params. device_id (required) names a device in your registry; min_confidence (optional, 0.01.0) returns 422 if the resolved envelope falls below your threshold. Point-based lookups by raw lat/lon belong to a different route — GET /v1/nearby (finds devices within radius_m of a point).

Request

curl -G https://api.geog.ai/v1/context \ -H "Authorization: Bearer $GEOG_API_KEY" \ --data-urlencode "device_id=sensor_h2s_023"
# pip install geog-ai from geog_ai import GeogClient geog = GeogClient(api_key=os.environ["GEOG_API_KEY"]) ctx = geog.context(device_id="sensor_h2s_023") print(ctx["result"]["context"]) # terrain, met, jurisdiction, … print(ctx["confidence"]) # 0.0–1.0 confidence for this envelope
// npm i @geog-ai/sdk import { GeogClient } from "@geog-ai/sdk"; const geog = new GeogClient({ apiKey: process.env.GEOG_API_KEY! }); const ctx = await geog.context({ device_id: "sensor_h2s_023" }); console.log(ctx.result.context); // terrain, met, jurisdiction, … console.log(ctx.confidence); // 0.0–1.0 confidence for this envelope
Edit the request, hit Send, see a live or simulated response.

Response

The top-level envelope carries result (the resolved SpatialState), confidence, model_version, calibration_age, and a provenance block listing which data sources were consulted and which were degraded (unavailable for this location). Fields inside result.context are null when the corresponding layer is degraded.

Example response JSON
{
  "result": {
    "entity_id": "sensor_h2s_023",
    "location": { "lat": 39.5, "lon": -104.5, "alt_m": null, "h3_index": "89268c12143ffff", "indoor": null },
    "time": "2026-04-21T03:14:22Z",
    "context": {
      "wind_vector": null,
      "terrain": null,
      "atmospheric_stability": null,
      "nearby_entities": [],
      "jurisdiction": null,
      "land_use": null,
      "infrastructure": { "kind": "rail", "classification": "freight_main_line", "source_dataset": "bootstrap" }
    }
  },
  "confidence": 0.35,
  "model_version": "geog-phase1-v1",
  "calibration_age": null,
  "provenance": {
    "sources": ["jurisdiction", "nearby_entities", "infrastructure"],
    "degraded": ["jurisdiction", "terrain", "atmospheric", "land_use"],
    "as_of": "2026-04-21T03:14:22Z"
  }
}
That's a real, deterministic success. No model was asked to reason — the API read your device's registered location and overlaid whatever spatial layers are available. A higher confidence and fewer degraded layers mean the site has richer calibrated data. Next you'll feed a location into a physics simulation.
3
Run a simulation with POST /simulate/flood
~3 min

POST /v1/simulate/{engine} runs a deterministic physics model. Here we use /simulate/flood; the same shape works for plume, fire, noise, thermal, and rf_coverage. Simulations are asynchronous: the submit returns 202 Accepted with a job_id, and you poll the job until it succeeds. This call needs a key with the simulate scope.

Key request fields

FieldTypeRequiredDescription
site_idstringRequiredIdentifier for the site being simulated
vertical_idstringRequiredVertical entitled on your key, matching V\d\d (e.g. V01)
scenarioobjectRequiredEngine-specific inputs (for flood: precip_total_mm, terrain, …)
tier_capstringOptionalExplicit tier cap, e.g. tier1_only
callback_urlstringOptionalHTTPS callback invoked with the HMAC-signed result on terminal status

Submit the job

curl -X POST https://api.geog.ai/v1/simulate/flood \ -H "Authorization: Bearer $GEOG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "site_id": "site_default", "vertical_id": "V01", "scenario": { "precip_total_mm": 50.0, "terrain": "dem" } }'
accepted = geog.simulate_flood({ "site_id": "site_default", "vertical_id": "V01", "scenario": {"precip_total_mm": 50.0, "terrain": "dem"}, }) job_id = accepted["job_id"] # flat 202 body: {"job_id", "status", ...} print("submitted", job_id)
const accepted = await geog.simulateFlood({ site_id: "site_default", vertical_id: "V01", scenario: { precip_total_mm: 50.0, terrain: "dem" }, }); const jobId = (accepted as { job_id: string }).job_id; // flat 202 body console.log("submitted", jobId);
Tweak the scenario, then Send.

Accepted response (202)

The submit returns a flat acknowledgement — no result yet. Note status_url and result_url: those are the live paths you poll.

Accepted response (202) JSON
{
  "job_id": "job_8xkd92mzp1",
  "status": "pending",
  "engine": "flood",
  "result_url": "/v1/simulate/jobs/job_8xkd92mzp1/result",
  "status_url": "/v1/simulate/jobs/job_8xkd92mzp1",
  "idempotent_replay": false
}

Poll the job until it succeeds

Poll GET /v1/simulate/jobs/{job_id} until status is terminal — succeeded, failed, or expired — then fetch the result envelope from GET /v1/simulate/jobs/{job_id}/result. The SDK wait_for_job / waitForJob helpers do both for you.

Poll the simulate job path, not /v1/jobs/{id}. The generic /v1/jobs/{id} path appears in older docs and the OpenAPI spec but is not served by the live API. Always poll GET /v1/simulate/jobs/{job_id} and read results at GET /v1/simulate/jobs/{job_id}/result.
# 1. Poll status until "succeeded" curl https://api.geog.ai/v1/simulate/jobs/job_8xkd92mzp1 \ -H "Authorization: Bearer $GEOG_API_KEY" # 2. Once succeeded, fetch the result envelope curl https://api.geog.ai/v1/simulate/jobs/job_8xkd92mzp1/result \ -H "Authorization: Bearer $GEOG_API_KEY"
# Polls GET /v1/simulate/jobs/{id}, then fetches /result on success done = geog.wait_for_job(job_id, interval=2.0, timeout=300) if done["job"]["status"] == "complete": # live "succeeded" → "complete" print(done["result"]) else: raise RuntimeError(f"job {job_id} did not succeed")
// Polls GET /v1/simulate/jobs/{id}, then fetches /result on success const done = await geog.waitForJob(jobId, { intervalMs: 2000, timeoutMs: 300_000 }); if (done.job.status === "complete") { // live "succeeded" → "complete" console.log(done.result); } else { throw new Error(`job ${jobId} did not succeed`); }

Job status response

GET /v1/simulate/jobs/{job_id} JSON
{
  "job_id": "job_8xkd92mzp1",
  "status": "succeeded",  // pending | running | succeeded | failed | expired
  "engine": "flood",
  "submitted_at": "2026-04-21T03:14:22Z",
  "completed_at": "2026-04-21T03:14:31Z",
  "cpu_seconds": 2.4,
  "result_url": "/v1/simulate/jobs/job_8xkd92mzp1/result"
}
You just ran real physics. This deterministic submit → poll → result pattern works for every simulation engine — /simulate/plume, /simulate/fire, /simulate/rf_coverage, and more — and for the placement optimizers under /optimize/*.
4
AI analysis with POST /analyze
~2 min
This step is optional and clearly separate from the deterministic core. Steps 2–3 were pure physics. The AI Spatial Analyst adds a natural-language layer on top of the same capability registry — but the LLM never invents spatial math and no LLM-generated code executes.

POST /v1/analyze takes a plain-language query and an execution mode. An LLM planner translates your question into a SpatialAnalysisPlan constrained to registered capabilities, then that plan is validated (schema → capabilities → inputs → cost) before anything runs.

plan vs. auto — and the scope each needs

  • execution: "plan" (default) — returns a validated plan only; nothing executes. Requires the read scope. This is the safe default: inspect the steps, capabilities, and estimated cost before committing.
  • execution: "auto" — validates the plan then executes it deterministically through the capability layer. Requires the simulate scope (a read-only key gets 403), because auto-execution can submit compute jobs.

Request (execution: "plan")

curl -X POST https://api.geog.ai/v1/analyze \ -H "Authorization: Bearer $GEOG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "What is the flood risk within 2km of coordinate 39.5, -104.5?", "execution": "plan" }'
plan = geog.analyze( "What is the flood risk within 2km of coordinate 39.5, -104.5?", execution="plan", # plan-only; needs the "read" scope ) print(plan["plan"]["steps"]) # the validated capability steps print(plan["planner"]["token_cost_units"]) # To validate AND run it (needs the "simulate" scope): # executed = geog.analyze(query, execution="auto")
const plan = await geog.analyze( "What is the flood risk within 2km of coordinate 39.5, -104.5?", { execution: "plan" }, // plan-only; needs the "read" scope ); console.log(plan.plan.steps); // the validated capability steps console.log(plan.planner.token_cost_units); // To validate AND run it (needs the "simulate" scope): // const executed = await geog.analyze(query, { execution: "auto" });
Edit the query, hit Send, inspect the validated plan.

Response

The response carries mode ("plan" or "auto"), the validated plan (objective, capability steps, estimated_cost, and a validation record), and a planner block with the model used and LLM token cost — metered against your account as planner.token_cost_units. In auto mode the plan's step_results are populated and a trust_level is assigned after execution.

You've completed the journey. Sign up → key → install → a deterministic first call, a real simulation with job polling, and an AI-planned analysis. Agents and MCP-compatible tools (Claude, Cursor, …) can also connect directly to the MCP server at POST /v1/mcp — see the MCP connection guide.

Three access levels

Every geog.ai computation runs through the same deterministic capability layer — you choose how much abstraction you want on top of it:

1. Raw endpoints

Direct calls to individual endpoints (/v1/context, /v1/simulate/flood, /v1/terrain/profile, …). Maximum control; you handle orchestration. This quickstart uses this level.

2. Capability API

Discover registered capabilities at GET /v1/capabilities (public), then run one with explicit inputs:

POST /v1/capabilities/terrain_profile/execute
{"inputs": {"start": {"lat": 31.9, "lon": -99.9}, "end": {"lat": 32.0, "lon": -99.8}}}

Or compose multiple steps into a serializable, auditable SpatialAnalysisPlan via POST /v1/plans and execute it deterministically. Every result carries a validation record, dataset provenance, and a trust level. SDKs: client.capabilities.execute(...) in both Python and TypeScript.

3. Natural-language API (AI Spatial Analyst)

Ask in plain language; an LLM planner translates your question into a plan constrained to registered capabilities, which is then validated (schema → capabilities → inputs → cost) before anything runs. The LLM never invents spatial math and no LLM-generated code executes.

POST /v1/analyze
{"query": "What's the elevation profile between these two points?",
 "execution": "plan"}   // "plan" = return validated plan only; "auto" = validate then execute

SDKs: client.analyze("...", execution="auto") / client.analyze("...", { execution: "auto" }). LLM token cost is metered against your account and reported in planner.token_cost_units.

Agents and MCP-compatible tools (Claude, Cursor, etc.) can also connect directly to the MCP server at POST /v1/mcp — see the MCP connection guide.

Troubleshooting

Hit a snag on your first run? Here are the four issues developers report most often, the exact error envelope you'll see, and how to fix each one. Every error response uses the same shape — an ok: false envelope with a machine-readable error.code, a human-readable error.message, and a request_id you can quote when emailing support.

401 Unauthorized — INVALID_API_KEY

The API key is missing, malformed, expired, or revoked. The most common cause is forgetting the Bearer prefix or shell-quoting issues that strip the variable.

Error response JSON
HTTP/1.1 401 Unauthorized
{
  "ok": false,
  "error": {
    "code": "INVALID_API_KEY",
    "message": "API key missing, expired, or revoked. Check the Authorization header is 'Bearer geog_(test|live)_sk_…'.",
    "request_id": "req_01hx2bsknd4bqyt8r"
  }
}

Fix checklist:

  • Confirm the header is Authorization: Bearer <prefix>.<secret> — the literal word Bearer, a single space, then the whole dotted key string.
  • Echo the variable before sending: echo "$GEOG_API_KEY". An empty value usually means the env var was set in a different shell session — re-source ~/.zshrc or restart your terminal.
  • Send the entire key including the dot. Sending only the public prefix (the part before the .) returns INVALID_API_KEY — the secret half is what authenticates.
  • Rotated or revoked keys stop working instantly. If you regenerated the key at geog.ai/account, copy the new one — the old value will keep returning 401.

422 Validation error — INVALID_PARAMS

The request was authenticated but the body or query string failed validation. error.message always names the offending field.

Error response JSON
HTTP/1.1 422 Unprocessable Entity
{
  "ok": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "Field 'vertical_id' must match pattern '^V\\d{2}$' (got 'v1').",
    "request_id": "req_01hx2csknd4bqyt8r"
  }
}

Fix checklist:

  • /context takes device_id, not coordinates. A raw lat/lon lookup belongs to GET /v1/nearby — pointing coordinates at /context returns a validation or 404 device_not_found error.
  • vertical_id must match V\d\d (e.g. V01) on POST /v1/simulate/*, and the vertical must be entitled on your key.
  • Send numbers, not strings. JSON "50.0" (with quotes) fails where a number is expected — use 50.0.
  • /nearby radius_m is capped at 10 000 m in Phase 1; larger values return radius_exceeds_phase1_cap.
  • An unknown simulation engine returns 404 with UNSUPPORTED_CAPABILITY and lists the available engines.

Job never reaches succeeded

Your POST /v1/simulate/flood returned 202 with a job_id, but polling never flips to succeeded. Poll the live status path — GET /v1/simulate/jobs/{job_id} — and read the status field directly.

curl https://api.geog.ai/v1/simulate/jobs/job_8xkd92mzp1 \   -H "Authorization: Bearer $GEOG_API_KEY"

Fix checklist:

  • Poll /v1/simulate/jobs/{job_id}, not /v1/jobs/{id}. The generic /v1/jobs/{id} path is documented-only and is not served by the live API — polling it returns 404 forever. The SDK wait_for_job / waitForJob helpers already target the correct path.
  • Terminal statuses are succeeded, failed, and expired — not "complete". Read the result envelope from GET /v1/simulate/jobs/{job_id}/result only after status == "succeeded"; that route returns 404 (with retryable: true) until the job succeeds.
  • A 404 job_not_found on the status route usually means you're authenticating as a different tenant than the one that submitted the job — jobs are scoped to the submitting tenant to prevent cross-tenant reads.
  • Cancelled jobs land in expired via DELETE /v1/simulate/jobs/{job_id}. If your poll loop sees expired, someone cancelled the job.
  • Optional callbacks: supply callback_url (HTTPS) on the submit to receive an HMAC-signed result on terminal status instead of polling.

Low confidence — confidence < 0.6

This isn't an error — the request succeeded with HTTP 200 — but the envelope's top-level confidence signals that key spatial layers were unavailable for this location. Treat the result as directional, not actionable, and don't trigger automated alerts on it. The provenance.degraded list tells you exactly which layers were missing.

Low-confidence response JSON
HTTP/1.1 200 OK
{
  "result": { /* … SpatialState … */ },
  "confidence": 0.35,
  "model_version": "geog-phase1-v1",
  "calibration_age": null,
  "provenance": {
    "sources": ["jurisdiction", "nearby_entities", "infrastructure"],
    "degraded": ["jurisdiction", "terrain", "atmospheric", "land_use"]
  }
}

What the score means:

  • ≥ 0.85 — site-calibrated with full layer coverage, safe for automated decisions and alerting.
  • 0.60 – 0.85 — uncalibrated but well-supported; surface to humans, gate auto-actions.
  • < 0.60 — several layers degraded; directional only. Show a "low confidence" badge and require manual review.

How to raise confidence:

  • Register and calibrate the site. A location with no calibration (calibration_age: null, as above) is the single biggest driver of a low score — most sites move from ~0.35 to 0.9+ after a single calibration pass.
  • Read provenance.degraded. Each entry is a layer the API couldn't resolve for this location (e.g. terrain, atmospheric). Supplying or enabling those datasets for the site removes them from the list and lifts confidence.
  • Check coverage: on the edge of a supported region parts of the SpatialState fall back to coarser data and confidence drops. Set a floor with the min_confidence query param to have /context return 422 instead of a weak envelope.
  • In code, gate on confidence explicitly — e.g. if ctx["confidence"] < 0.6: queue_for_review() — rather than treating every 200 OK as actionable.
Need a code that isn't listed here? The complete catalogue — every HTTP status, error code, and retry guidance — is in the Errors section of the API reference. Always include the request_id from the response when you contact support — it lets us pull the full trace for your call in one query.