Error format
Every error response is JSON with a human-readable error message that’s safe to show to users:
{ "error": "You already voted this way on this spot" }Validation failures on POST /api/spots also include issues, in
zod’s issue format, so you can highlight the field:
{
"error": "Location must be inside Uganda",
"issues": [{ "code": "custom", "path": ["lat"], "message": "Location must be inside Uganda" }]
}Status codes
| Code | Meaning | What to do |
|---|---|---|
200 / 201 |
Success | |
400 |
Invalid input: bad JSON, a missing field, out-of-range values, or a location outside Uganda | Fix the request. Don’t retry unchanged. |
404 |
The spot doesn’t exist or isn’t public (pending or rejected) | Refresh your spot list. |
409 |
Already voted this way, or already flagged | Treat it as success in your UI. |
429 |
Rate limit reached | Wait Retry-After seconds. See Rate limits. |
502 |
The routing service (OSRM) is unavailable | Retry later with backoff. Search degrades instead: it returns spots with an error note. |
503 |
The health check can’t reach the database | Check the deployment. |
Rate limits
Every endpoint except /api/health is rate limited, and responses carry RateLimit-* headers.
See Rate limits for the limits per endpoint and how to back off.
Validation rules
| Field | Rule |
|---|---|
title |
3–80 characters |
description, area |
Up to 600 and 80 characters. Optional. |
lat, lng |
Inside Uganda: lat −1.6 to 4.3, lng 29.5 to 35.1 |
category |
One of murder, mob_action, boda_gang, robbery, stabbing, kidnapping, other |
severity |
Integer 1–5 |
time_of_day |
day, night or any |
incident_date |
YYYY-MM-DD, or null |
source_url |
An http(s) URL up to 300 characters, "" or null |
note (flags) |
Up to 300 characters |
Phone numbers in title, description, area and flag notes are replaced with [removed].