REST API Reference
This document provides a complete reference for Visita’s REST API endpoints, including authentication, request/response formats, and example usage.
Base URL: https://visita-intelligence.vercel.app/api
Authentication
All protected API endpoints require authentication using Bearer tokens.
Authorization: Bearer <supabase_access_token>
Content-Type: application/json
Obtaining a Token
Tokens are obtained through Supabase Auth:
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'password'
});
// Get session
const { data: { session } } = await supabase.auth.getSession();
const token = session?.access_token;
Ward Intelligence API
Get Ward Overview
Description: Retrieve comprehensive ward intelligence including safety, weather, and community data.
Path Parameters:
ward_code (string, required): Ward identifier (e.g., “WARD001”)
Query Parameters:
include_weather (boolean, optional): Include weather data (default: true)
include_safety (boolean, optional): Include safety statistics (default: true)
include_community (boolean, optional): Include community projects (default: true)
Response:
{
"ward_code": "WARD001",
"name": "Ward 1 - Johannesburg Central",
"boundaries": {
"type": "Polygon",
"coordinates": [[...]]
},
"statistics": {
"business_count": 245,
"project_count": 12,
"member_count": 1847,
"safety_score": 7.2
},
"weather": {
"current": {
"temperature": 22,
"condition": "Partly Cloudy",
"humidity": 65,
"wind_speed": 15
},
"forecast": [...]
},
"safety": {
"overall_score": 7.2,
"crime_trend": "decreasing",
"recent_incidents": [...]
},
"community": {
"active_projects": [...],
"recent_updates": [...]
}
}
Example Request:
curl -X GET "https://visita-intelligence.vercel.app/api/ward/WARD001?include_weather=true&include_safety=true" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"
Status Codes:
200 - Success
401 - Unauthorized
404 - Ward not found
500 - Internal server error
Get Ward Signals
GET /ward/{ward_code}/signals
Description: Retrieve recent signals (alerts, incidents, updates) for a ward.
Path Parameters:
ward_code (string, required): Ward identifier
Query Parameters:
type (string, optional): Filter by signal type (safety, community, weather, infrastructure)
limit (number, optional): Number of results (default: 20, max: 100)
offset (number, optional): Pagination offset (default: 0)
time_range (string, optional): Time range (1h, 24h, 7d, 30d, default: 24h)
Response:
{
"signals": [
{
"id": "signal-123",
"type": "safety",
"title": "Power Outage Reported",
"description": "Power outage affecting 3 blocks...",
"severity": "medium",
"location": {
"lat": -26.2041,
"lng": 28.0473
},
"timestamp": "2025-12-30T15:30:00Z",
"source": "user_report",
"corroborations": 5,
"status": "active"
}
],
"pagination": {
"total": 42,
"limit": 20,
"offset": 0,
"has_more": true
}
}
Example Request:
curl -X GET "https://visita-intelligence.vercel.app/api/ward/WARD001/signals?type=safety&limit=10&time_range=24h" \
-H "Authorization: Bearer $TOKEN"
Business Directory API
Search Businesses
Description: Search businesses with filtering and geospatial queries.
Query Parameters:
q (string, optional): Search query
ward_code (string, optional): Filter by ward
category (string, optional): Filter by category
lat (number, optional): Latitude for proximity search
lng (number, optional): Longitude for proximity search
radius (number, optional): Search radius in meters (default: 5000)
limit (number, optional): Number of results (default: 20)
offset (number, optional): Pagination offset
Response:
{
"businesses": [
{
"id": "business-123",
"name": "Maboneng Coffee",
"description": "Local coffee shop and community hub",
"category": "Food & Beverage",
"ward_code": "WARD001",
"location": {
"lat": -26.2041,
"lng": 28.0473,
"address": "123 Main St, Johannesburg"
},
"contact": {
"phone": "+27123456789",
"email": "[email protected]",
"website": "https://mabonengcoffee.co.za"
},
"rating": 4.5,
"review_count": 127,
"opening_hours": {...},
"service_areas": ["WARD001", "WARD002"]
}
],
"pagination": {
"total": 245,
"limit": 20,
"offset": 0,
"has_more": true
},
"facets": {
"categories": [
{"name": "Food & Beverage", "count": 45},
{"name": "Retail", "count": 38}
],
"wards": [
{"ward_code": "WARD001", "count": 67},
{"ward_code": "WARD002", "count": 54}
]
}
}
Example Request:
curl -X GET "https://visita-intelligence.vercel.app/api/business/search?q=coffee&ward_code=WARD001&lat=-26.2041&lng=28.0473&radius=2000" \
-H "Authorization: Bearer $TOKEN"
Get Business Details
Description: Retrieve detailed information about a specific business.
Path Parameters:
id (string, required): Business ID or slug
Response:
{
"id": "business-123",
"name": "Maboneng Coffee",
"slug": "maboneng-coffee",
"description": "Local coffee shop and community hub...",
"category": "Food & Beverage",
"subcategory": "Coffee Shop",
"ward_code": "WARD001",
"location": {
"lat": -26.2041,
"lng": 28.0473,
"address": "123 Main St, Johannesburg, 2001",
"ward": "WARD001"
},
"contact": {
"phone": "+27123456789",
"email": "[email protected]",
"website": "https://mabonengcoffee.co.za"
},
"social": {
"facebook": "mabonengcoffee",
"instagram": "mabonengcoffee"
},
"rating": 4.5,
"review_count": 127,
"opening_hours": {
"monday": "08:00-18:00",
"tuesday": "08:00-18:00",
"sunday": "09:00-15:00"
},
"photos": [
"https://cdn.visita.co.za/photos/business-123/1.jpg"
],
"products": [
{
"id": "product-1",
"name": "Cappuccino",
"price": 35,
"currency": "ZAR"
}
],
"service_areas": [
{
"ward_code": "WARD001",
"service_type": "primary"
},
{
"ward_code": "WARD002",
"service_type": "delivery"
}
],
"verified": true,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-12-20T15:30:00Z"
}
GET /ward/{ward_code}/projects
Description: Retrieve community action projects for a ward.
Path Parameters:
ward_code (string, required): Ward identifier
Query Parameters:
status (string, optional): Filter by status (active, completed, planned)
category (string, optional): Filter by category
limit (number, optional): Number of results (default: 20)
offset (number, optional): Pagination offset
Response:
{
"projects": [
{
"id": "project-123",
"title": "Community Garden Initiative",
"description": "Establishing a community garden...",
"ward_code": "WARD001",
"category": "environment",
"status": "active",
"progress": 65,
"creator": {
"id": "user-456",
"name": "Sarah Johnson",
"avatar": "https://..."
},
"participants": 24,
"pledges": {
"total_amount": 12500,
"currency": "ZAR",
"count": 18
},
"milestones": [
{
"id": "milestone-1",
"title": "Secure Land",
"status": "completed",
"completed_at": "2025-11-15T10:00:00Z"
},
{
"id": "milestone-2",
"title": "Install Irrigation",
"status": "in_progress",
"due_date": "2025-12-31T23:59:59Z"
}
],
"created_at": "2025-10-01T10:00:00Z",
"updated_at": "2025-12-28T15:30:00Z"
}
],
"pagination": {
"total": 12,
"limit": 20,
"offset": 0,
"has_more": false
}
}
Get Governance Topics
GET /ward/{ward_code}/governance/topics
Description: Retrieve governance discussion topics for a ward.
Path Parameters:
ward_code (string, required): Ward identifier
Query Parameters:
status (string, optional): Filter by status (open, closed, archived)
limit (number, optional): Number of results (default: 20)
offset (number, optional): Pagination offset
Response:
{
"topics": [
{
"id": "topic-123",
"title": "Budget Allocation for 2026",
"description": "Discussion on how to allocate...",
"ward_code": "WARD001",
"status": "open",
"type": "budget",
"creator": {
"id": "user-456",
"name": "Councillor Smith",
"role": "councillor"
},
"statements": [
{
"id": "statement-1",
"author": {...},
"content": "I propose we prioritize infrastructure...",
"votes": {
"agree": 45,
"disagree": 12,
"abstain": 8
},
"timestamp": "2025-12-28T10:00:00Z"
}
],
"consensus_score": 78.5,
"participant_count": 65,
"created_at": "2025-12-01T10:00:00Z",
"closes_at": "2026-01-15T23:59:59Z"
}
],
"pagination": {
"total": 8,
"limit": 20,
"offset": 0,
"has_more": false
}
}
AI & Intelligence API
Ask Ward Question (RAG)
POST /ward/{ward_code}/ask
Description: Ask a question about ward intelligence using the RAG pipeline.
Path Parameters:
ward_code (string, required): Ward identifier
Request Body:
{
"question": "What are the current safety concerns?",
"max_sources": 5,
"tier": "mid"
}
Request Body Parameters:
question (string, required): The question to ask
max_sources (number, optional): Maximum number of source documents (default: 5, max: 10)
tier (string, optional): AI model tier (cheap, mid, quality, default: mid)
Response:
{
"answer": "Based on recent reports, Ward 1 has seen a 15% decrease in crime over the past quarter. However, there are ongoing concerns about power outages affecting the northern section, with 3 incidents reported in the last week. The community has organized neighborhood watch programs that have been effective in reducing petty crime.",
"sources": [
{
"id": "doc-123",
"title": "Q4 2025 Safety Report",
"snippet": "Crime statistics show 15% decrease...",
"url": "/reports/safety-q4-2025",
"timestamp": "2025-12-20T10:00:00Z"
},
{
"id": "doc-124",
"title": "Power Outage Log - Week 52",
"snippet": "3 outages reported in northern section...",
"url": "/logs/power-outages-2025-w52",
"timestamp": "2025-12-28T15:30:00Z"
}
],
"metadata": {
"model_used": "claude-3-haiku-20240307",
"tokens_used": 542,
"documents_retrieved": 2,
"generated_at": "2025-12-30T16:00:00Z"
}
}
Example Request:
curl -X POST "https://visita-intelligence.vercel.app/api/ward/WARD001/ask" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"question": "What are the current safety concerns?",
"max_sources": 5,
"tier": "mid"
}'
Map Tiles API
Get Map Tile
Description: Retrieve map tiles for ward visualization.
Path Parameters:
z (number, required): Zoom level
x (number, required): Tile X coordinate
y (number, required): Tile Y coordinate
Query Parameters:
layer (string, optional): Layer type (wards, businesses, signals, default: wards)
style (string, optional): Map style (light, dark, satellite, default: light)
Response:
Returns a PNG or WebP image tile.
Example Request:
curl -X GET "https://visita-intelligence.vercel.app/api/tiles/14/12345/67890?layer=wards&style=light" \
-H "Authorization: Bearer $TOKEN"
Error Responses
All API endpoints return consistent error responses:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid ward_code format",
"details": {
"field": "ward_code",
"value": "INVALID",
"constraint": "Must match pattern ^WARD\\d{3}$"
}
}
}
Common Error Codes:
| Code | HTTP Status | Description |
|---|
VALIDATION_ERROR | 400 | Request validation failed |
UNAUTHORIZED | 401 | Authentication required or invalid |
FORBIDDEN | 403 | Insufficient permissions |
NOT_FOUND | 404 | Resource not found |
RATE_LIMITED | 429 | Rate limit exceeded |
INTERNAL_ERROR | 500 | Internal server error |
Rate Limiting
API requests are rate-limited to ensure fair usage:
- Authenticated requests: 1000 requests per hour per user
- Unauthenticated requests: 100 requests per hour per IP
Headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
X-RateLimit-Reset: 1640995200
List endpoints support cursor-based pagination:
Query Parameters:
limit (number): Number of results per page (default: 20, max: 100)
offset (number): Number of results to skip (default: 0)
Response:
{
"data": [...],
"pagination": {
"total": 245,
"limit": 20,
"offset": 0,
"has_more": true
}
}
Example Pagination:
# First page
curl "https://visita-intelligence.vercel.app/api/business/search?limit=20&offset=0"
# Second page
curl "https://visita-intelligence.vercel.app/api/business/search?limit=20&offset=20"
# Third page
curl "https://visita-intelligence.vercel.app/api/business/search?limit=20&offset=40"
SDKs & Libraries
TypeScript/JavaScript
import { VisitaAPI } from '@visita/sdk';
const client = new VisitaAPI({
baseURL: 'https://visita-intelligence.vercel.app/api',
token: 'your-supabase-token'
});
// Get ward overview
const ward = await client.wards.get('WARD001');
// Search businesses
const businesses = await client.businesses.search({
q: 'coffee',
ward_code: 'WARD001'
});
// Ask AI question
const answer = await client.ai.ask('WARD001', {
question: 'What are the safety concerns?'
});
Testing
Test the API using the provided sandbox:
# Get a test token
curl -X POST "https://visita-intelligence.vercel.app/api/auth/test-token" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'
# Use the token in subsequent requests
TOKEN="your-test-token"
curl -H "Authorization: Bearer $TOKEN" \
"https://visita-intelligence.vercel.app/api/ward/WARD001"
Test tokens expire after 1 hour and can only access test data.
Support
For API support: