API Reference
The GeoContext API gives your AI agent a sense of place — ranked semantic POI results, neighbourhood profiles, weather, and timezone in a single call.
job_id (HTTP 202). Poll GET /jobs/{job_id} for the result.
Authentication
Every public endpoint (except GET /health) requires a Bearer token:
Authorization: Bearer <your_api_token>
| Status | Cause |
|---|---|
| 401 | Missing or malformed Authorization header |
| 403 | Invalid token |
Async Job Pattern
All submit endpoints return a job_id immediately. The request is processed asynchronously by the API backend. Client polls until the result is ready.
Submit
POST your request. Receive job_id in a 202 response instantly.
Poll
GET /jobs/{id} every second. Status moves from queued → processing.
Result
Status becomes complete. Full result in the same response body.
Job statuses
| Status | Meaning |
|---|---|
| queued | Accepted, awaiting processing |
| processing | Being processed by the API backend |
| complete | Result available in result field |
| error | Execution failed; see error field |
Recommended polling strategy
1. Submit → receive job_id
2. Wait 500 ms
3. Poll GET /jobs/{job_id} every 1 s
4. Stop after 120 s (service may be unavailable)
POST /forward
Request body
{
"query": "wheelchair-accessible Italian restaurant",
"lat": 52.5200,
"lon": 13.4050,
"radius_m": 1000,
"limit": 10,
"neighborhood": false,
"weather": false,
"map": false
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| query | string | yes | — | Natural language search query |
| lat | float | yes | — | Latitude (-90 to 90) |
| lon | float | yes | — | Longitude (-180 to 180) |
| radius_m | int | no | 1000 | Search radius in metres |
| limit | int | no | 10 | Max results (1–50) |
| neighborhood | bool | no | false | Include neighbourhood POI profile |
| weather | bool | no | false | Include current weather |
| map | bool | no | false | Include static map image URL |
Response
{
"job_id": "3f8e2a1b-4c9d-4e5f-8a1b-2c3d4e5f6a7b",
"status": "complete",
"result": {
"query": "wheelchair-accessible Italian restaurant",
"location": { "lat": 52.52, "lon": 13.405, "display_name": "Berlin, Germany" },
"results": [
{
"osm_id": 123456789,
"osm_type": "node",
"name": "Trattoria Roma",
"description": "Italian restaurant. Wheelchair accessible. Outdoor seating.",
"lat": 52.5198,
"lon": 13.4048,
"distance_m": 45,
"relevance_score": 0.91,
"combined_score": 0.87,
"tags": { "amenity": "restaurant", "cuisine": "italian", "wheelchair": "yes" },
"opening_hours": "Mo-Su 12:00-22:00",
"is_open": true,
"confidence": 0.95
}
],
"neighborhood": null,
"weather": null,
"suggested_queries": ["nearby parking", "wheelchair route to restaurant"]
}
}
curl example
curl -X POST https://api.YOUR_DOMAIN/forward \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "wheelchair-accessible Italian restaurant",
"lat": 52.52, "lon": 13.405
}'
# → { "job_id": "3f8e...", "status": "queued" }
curl https://api.YOUR_DOMAIN/jobs/3f8e2a1b-... \
-H "Authorization: Bearer $API_TOKEN"
# → { "job_id": "...", "status": "complete", "result": { ... } }
POST /neighbourhood
Request body
{
"lat": 52.5200,
"lon": 13.4050,
"radius_m": 500
}
Response result
{
"lat": 52.52,
"lon": 13.405,
"radius_m": 500,
"categories": {
"restaurant": 12,
"cafe": 8,
"transit_stop": 4,
"park": 2,
"pharmacy": 1
},
"walkability": "high",
"summary": "Dense mixed-use urban area with strong food/drink and transit presence."
}
POST /reverse
Request body
{
"lat": 52.5200,
"lon": 13.4050,
"neighborhood": false,
"weather": false,
"map": false
}
Response result
{
"lat": 52.52,
"lon": 13.405,
"display_name": "Unter den Linden 1, Mitte, Berlin, 10117, Germany",
"address": {
"road": "Unter den Linden",
"house_number": "1",
"suburb": "Mitte",
"city": "Berlin",
"postcode": "10117",
"country": "Germany",
"country_code": "de"
},
"neighborhood": null,
"weather": null
}
POST /context
Request body
{
"lat": 52.5200,
"lon": 13.4050
}
Response result
{
"lat": 52.52,
"lon": 13.405,
"address": {
"display_name": "Mitte, Berlin, Germany",
"country_code": "de"
},
"timezone": {
"iana": "Europe/Berlin",
"utc_offset_h": 2,
"dst": true
},
"weather": {
"current": { "temp_c": 18.5, "condition": "Partly cloudy" },
"forecast": [ { "date": "2026-05-28", "max_c": 21, "min_c": 12 } ]
},
"neighborhood": {
"categories": { "restaurant": 12, "cafe": 8 },
"walkability": "high"
}
}
GET /weather
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| lat | float | yes | Latitude |
| lon | float | yes | Longitude |
Example
curl "https://api.YOUR_DOMAIN/weather?lat=52.52&lon=13.405" \ -H "Authorization: Bearer $API_TOKEN"
Response result
{
"lat": 52.52,
"lon": 13.405,
"timezone": "Europe/Berlin",
"current": {
"temp_c": 18.5,
"feels_like_c": 17.2,
"humidity_pct": 62,
"wind_kph": 14,
"condition": "Partly cloudy",
"is_day": true
},
"forecast": [
{
"date": "2026-05-28",
"max_c": 21,
"min_c": 12,
"condition": "Sunny",
"precip_mm": 0
}
]
}
GET /timezone
Query parameters
GET /timezone?lat=52.52&lon=13.405
Response result
{
"lat": 52.52,
"lon": 13.405,
"iana": "Europe/Berlin",
"utc_offset_h": 2,
"dst": true
}
GET /changes
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| since | int | yes | Unix timestamp (seconds) |
| min_lat | float | yes | Bounding box south |
| max_lat | float | yes | Bounding box north |
| min_lon | float | yes | Bounding box west |
| max_lon | float | yes | Bounding box east |
| limit | int | no | Max results (default 100) |
Response result
{
"count": 3,
"entities": [
{
"osm_id": 123,
"osm_type": "node",
"last_modified_ts": 1748390400,
"name": "Café Berlino"
}
]
}
GET /jobs/{job_id}
Responses
{ "job_id": "3f8e...", "status": "queued" }
{ "job_id": "3f8e...", "status": "processing" }
{ "job_id": "3f8e...", "status": "complete", "result": { ... } }
{ "job_id": "3f8e...", "status": "error", "error": "upstream error" }
GET /jobs/{id} returns 404.
GET /health
{ "status": "ok" }
Rate Limits
Each Bearer token is subject to a sliding-window rate limit:
| Limit | Value |
|---|---|
| Requests per token | 10 per 60 seconds |
| Window type | Sliding (rolling per-second) |
When the limit is exceeded the server responds with HTTP 429:
{"detail": "rate limit exceeded"}
If the global job queue exceeds 500 pending jobs, new submissions also return HTTP 429:
{"detail": "queue full, try again later"}
Error Codes
| HTTP | Cause |
|---|---|
| 401 | Missing or malformed Authorization header |
| 403 | Invalid Bearer token |
| 404 | GET /jobs/{id} — job not found or expired |
| 422 | Request body validation error (missing required fields) |
| 429 | Rate limit exceeded or queue full — back off and retry |
| 502 | Backend returned an error processing this job |
| 503 | Service temporarily unavailable |
Polling Guide
The recommended client polling loop:
import httpx, time
BASE = "https://api.YOUR_DOMAIN"
HDRS = {"Authorization": "Bearer YOUR_TOKEN", "Content-Type": "application/json"}
def search(query: str, lat: float, lon: float) -> dict:
# 1. Submit
r = httpx.post(f"{BASE}/forward",
json={"query": query, "lat": lat, "lon": lon},
headers=HDRS)
job_id = r.json()["job_id"]
# 2. Poll
time.sleep(0.5)
for _ in range(120):
r = httpx.get(f"{BASE}/jobs/{job_id}", headers=HDRS)
data = r.json()
if data["status"] == "complete":
return data["result"]
if data["status"] == "error":
raise RuntimeError(data.get("error", "unknown error"))
time.sleep(1.0)
raise TimeoutError("job did not complete within 120 s")
MCP Tool Example
Wrap POST /context as a Model Context Protocol tool to give any LLM a sense of place before inference:
# MCP tool definition
{
"name": "get_location_context",
"description": "Get rich geospatial context for a coordinate: address, timezone, weather, POI profile.",
"input_schema": {
"type": "object",
"properties": {
"lat": { "type": "number", "description": "Latitude" },
"lon": { "type": "number", "description": "Longitude" }
},
"required": ["lat", "lon"]
}
}
# Handler: POST /context → poll /jobs/{id} → return result as tool output
/context endpoint runs address, timezone, weather, and neighbourhood lookups in parallel — a single tool call gives the LLM full environmental awareness.
Pricing
| Tier | Price | Included requests | Overage |
|---|---|---|---|
| Free | $0 / mo | 3,000 | — |
| Starter | $49 / mo | 10,000 | $7.00 / 1,000 |
| Growth | $299 / mo | 100,000 | $3.08 / 1,000 |
| Pro | $999 / mo | 600,000 | $1.67 / 1,000 |
| Enterprise | Custom | Custom volume + SLA | — |
The free tier requires no credit card. Overage is billed at the per-request rate implied by the plan price after subtracting the 3,000 free requests included in all tiers.