API Endpoints Reference
Complete reference for GeoQuery's FastAPI endpoints.
Base URL
Development: http://localhost:8000
Production: Configure via environment
Endpoints
1. Chat Query (SSE Streaming)
Main endpoint for natural language queries with streaming responses.
POST /api/chat
Content-Type: application/json
Request Body:
{
"message": "Show me hospitals in Panama City",
"history": [
{"role": "user", "content": "previous query"},
{"role": "assistant", "content": "previous response"}
]
}
Response: Server-Sent Events (SSE) stream
Event Types:
status- Processing status updates{"status": "🔍 Identifying relevant tables..."}intent- Detected query intent{"intent": "MAP_REQUEST"}chunk- Streaming content (text or thought){"type": "thought", "content": "Planning spatial query..."} {"type": "text", "content": "I found 45 hospitals..."}result- Final result with data{ "response": "Full explanation text", "sql_query": "SELECT name, geom FROM ...", "geojson": {...}, "chart_data": {...}, "raw_data": [...], "data_citations": [...] }
curl Example:
curl -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-d '{"message": "Show me provinces", "history": []}' \
--no-buffer
JavaScript Example:
const response = await fetch('http://localhost:8000/api/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
message: "Show me hospitals",
history: []
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {value, done} = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
console.log(data);
}
}
}
2. Get Data Catalog
Returns metadata for all available datasets.
GET /api/catalog
Response:
{
"panama_healthsites_geojson": {
"description": "Healthcare facilities including hospitals...",
"categories": ["health", "infrastructure"],
"tags": ["hospitals", "clinics"],
"row_count": 986,
"has_geometry": true
},
"pan_admin1": {
"description": "Panama provinces...",
"categories": ["administrative"],
"row_count": 10,
"has_geometry": true
}
}
curl Example:
curl http://localhost:8000/api/catalog
3. Get Database Schema
Returns current database schema with loaded tables.
GET /api/schema
Response:
{
"tables": [
{
"name": "panama_healthsites_geojson",
"columns": ["osm_id", "name", "amenity", "geom"],
"row_count": 986,
"geometry_type": "Point"
},
{
"name": "pan_admin1",
"columns": ["adm1_name", "adm1_pcode", "area_sqkm", "geom"],
"row_count": 10,
"geometry_type": "Polygon"
}
],
"loaded_count": 2,
"total_datasets": 108
}
curl Example:
curl http://localhost:8000/api/schema
Response Formats
GeoJSON Structure
Map data returned in standard GeoJSON format with custom properties:
{
"type": "FeatureCollection",
"properties": {
"layer_id": "abc123",
"layer_name": "Hospitals in David",
"style": {
"color": "#E63946",
"fillColor": "#E63946",
"opacity": 0.8,
"fillOpacity": 0.4
},
"pointMarker": {
"icon": "🏥",
"style": "icon",
"color": "#E63946",
"size": 32
},
"choropleth": {
"enabled": false
}
},
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-79.5, 8.98]
},
"properties": {
"name": "Hospital Santo Tomás",
"amenity": "hospital"
}
}
]
}
Properties Explanation:
layer_id: Unique identifierlayer_name: Display namestyle: Default polygon/line stylingpointMarker: Point rendering configurationstyle: "icon"→ Use emoji iconstyle: "circle"→ Use simple circle
choropleth: Choropleth configuration (if enabled)
Chart Data Structure
{
"type": "bar",
"title": "Districts by Province",
"data": {
"labels": ["Panamá", "Chiriquí", "Veraguas"],
"datasets": [{
"label": "District Count",
"data": [8, 13, 12],
"backgroundColor": "#6366f1"
}]
}
}
Chart Types:
bar- Bar chartpie- Pie chartline- Line chart
Data Citations
[
"Administrative boundary data from HDX/INEC, 2021",
"Healthcare facilities from OpenStreetMap via Healthsites.io"
]
Error Responses
Standard Error Format
{
"detail": "Error message",
"error_type": "DataNotFound",
"timestamp": "2026-01-10T12:00:00Z"
}
HTTP Status Codes:
400 Bad Request- Invalid query format404 Not Found- Endpoint not found500 Internal Server Error- Server error503 Service Unavailable- LLM API unavailable
Data Unavailable Response
When requested data doesn't exist:
{
"response": "I couldn't find data for crime statistics in the current database...",
"sql_query": "-- ERROR: DATA_UNAVAILABLE\\n-- Requested: crime statistics\\n-- Available: admin boundaries, hospitals",
"geojson": null,
"data_citations": []
}
CORS Configuration
Current CORS settings (in backend/main.py):
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
For Production: Update allow_origins to your frontend domain.
Rate Limiting
Current: No rate limiting implemented
Recommended (future):
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@app.post("/api/chat")
@limiter.limit("10/minute")
async def chat(request: Request):
...
Authentication
Current: No authentication required
For Production: Add API key or JWT authentication:
from fastapi import Security, HTTPException
from fastapi.security import APIKeyHeader
API_KEY = os.getenv("API_KEY")
api_key_header = APIKeyHeader(name="X-API-Key")
def verify_api_key(api_key: str = Security(api_key_header)):
if api_key != API_KEY:
raise HTTPException(status_code=403, detail="Invalid API key")
return api_key
WebSocket Support
Current: Not implemented (using SSE instead)
Rationale: SSE sufficient for one-way server → client streaming. WebSockets add complexity without benefit for this use case.
API Versioning
Current: No versioning
Future Consideration:
/api/v1/chat
/api/v2/chat
OpenAPI Documentation
FastAPI auto-generates interactive API docs:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- OpenAPI JSON: http://localhost:8000/openapi.json
Client Libraries
Python Client Example
import requests
import json
def query_geoquery(message, history=[]):
response = requests.post(
"http://localhost:8000/api/chat",
json={"message": message, "history": history},
stream=True
)
for line in response.iter_lines():
if line.startswith(b'data: '):
data = json.loads(line[6:])
if data.get("event") == "chunk":
print(data["data"]["content"], end="")
elif data.get("event") == "result":
return data["data"]
result = query_geoquery("Show me hospitals")
print(result["sql_query"])
JavaScript/TypeScript Client
See frontend/src/lib/api.ts for full implementation.
Performance Considerations
Response Times
Typical query pipeline:
- Intent detection: ~500ms
- Semantic search: <10ms
- SQL generation: ~1s
- Query execution: 100ms - 5s (depends on data size)
- Explanation: ~1s (streaming)
Total: 2-8 seconds for most queries
Optimization Tips
- Pre-load Common Tables: Load frequently used datasets at startup
- Query Limits: Add
LIMITclauses for large result sets - Spatial Indexes: DuckDB auto-indexes geometry columns
- Caching: Consider Redis for repeated queries
Next Steps
- Core Services: CORE_SERVICES.md
- Data Flow: ../DATA_FLOW.md
- Frontend Components: ../frontend/COMPONENTS.md