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.
pip install geog-ai), and the TypeScript SDK (npm i @geog-ai/sdk). Pick whichever you prefer.
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.
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.
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:
.env file (excluded from git), or a secrets manager. A leaked key can be revoked instantly from your dashboard.
GET /contextYour 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.
device_id (required) names a device in your registry; min_confidence (optional, 0.0–1.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
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.
"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"
}
}
confidence and fewer degraded layers mean the site has richer calibrated data. Next you'll feed a location into a physics simulation.
POST /simulate/floodPOST /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
| Field | Type | Required | Description |
|---|---|---|---|
| site_id | string | Required | Identifier for the site being simulated |
| vertical_id | string | Required | Vertical entitled on your key, matching V\d\d (e.g. V01) |
| scenario | object | Required | Engine-specific inputs (for flood: precip_total_mm, terrain, …) |
| tier_cap | string | Optional | Explicit tier cap, e.g. tier1_only |
| callback_url | string | Optional | HTTPS callback invoked with the HMAC-signed result on terminal status |
Submit the job
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.
"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.
/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.
Job status response
"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"
}
/simulate/plume, /simulate/fire, /simulate/rf_coverage, and more — and for the placement optimizers under /optimize/*.
POST /analyzePOST /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 thereadscope. 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 thesimulatescope (a read-only key gets403), because auto-execution can submit compute jobs.
Request (execution: "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.
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.
{
"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 wordBearer, 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~/.zshrcor restart your terminal. - Send the entire key including the dot. Sending only the public prefix (the part before the
.) returnsINVALID_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.
{
"ok": false,
"error": {
"code": "INVALID_PARAMS",
"message": "Field 'vertical_id' must match pattern '^V\\d{2}$' (got 'v1').",
"request_id": "req_01hx2csknd4bqyt8r"
}
}
Fix checklist:
/contexttakesdevice_id, not coordinates. A rawlat/lonlookup belongs toGET /v1/nearby— pointing coordinates at/contextreturns a validation or404 device_not_founderror.vertical_idmust matchV\d\d(e.g.V01) onPOST /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 — use50.0. /nearbyradius_mis capped at 10 000 m in Phase 1; larger values returnradius_exceeds_phase1_cap.- An unknown simulation engine returns
404withUNSUPPORTED_CAPABILITYand 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.
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 returns404forever. The SDKwait_for_job/waitForJobhelpers already target the correct path. - Terminal statuses are
succeeded,failed, andexpired— not"complete". Read the result envelope fromGET /v1/simulate/jobs/{job_id}/resultonly afterstatus == "succeeded"; that route returns404(withretryable: true) until the job succeeds. - A
404 job_not_foundon 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
expiredviaDELETE /v1/simulate/jobs/{job_id}. If your poll loop seesexpired, 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.
{
"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.35to0.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_confidencequery param to have/contextreturn422instead of a weak envelope. - In code, gate on confidence explicitly — e.g.
if ctx["confidence"] < 0.6: queue_for_review()— rather than treating every200 OKas actionable.
request_id from the response when you contact support — it lets us pull the full trace for your call in one query.
Next steps
Now that you've made your first calls, here's where to go next: