Free · Open · No Auth Required
One API for all 36 states + FCT, 774 LGAs, GPS coordinates, NIPOST postal codes, geopolitical zones, routing, reverse geocoding, and proximity search — built for Nigerian developers tired of building location data from scratch.
{
"status": "success",
"meta": {
"state": {
"name": "Lagos",
"capital": "Ikeja",
"geo_zone": "SW",
"iso_code": "NG-LA"
},
"total": 20
},
"data": [
{
"name": "Agege",
"slug": "agege",
"state": "Lagos",
"coordinates": {
"lat": 6.6167,
"lng": 3.3167
},
"postal_code": "100283"
}
// ... 19 more LGAs
]
}
The Problem
Shipping a product in Nigeria means writing the same address form logic, the same state/LGA arrays, the same KYC dropdowns, the same Google Maps bill — over and over. This API ends that cycle.
API Reference
No API key. No registration. No rate limit headers. All responses return a consistent JSON envelope.
Response Envelope
{
"status": "success", // or "error"
"data": { ... },
"meta": { "total": 20 }
}
// Errors:
{
"status": "error",
"message": "State not found"
}
Slug resolution
All {slug} params accept slug (lagos), name (Lagos), or ISO code (NG-LA). Case-insensitive.
HTTP codes
200 Success · 404 Not found · 422 Validation error
| Param | Type | Required | Description |
|---|---|---|---|
| geo_zone | string | optional | Filter: NC NE NW SE SS SW |
Example
GET /api/v1/states?geo_zone=SW
{
"status": "success",
"data": [{
"id": 25, "name": "Lagos", "slug": "lagos",
"capital": "Ikeja", "geo_zone": "SW",
"geo_zone_full": "South West", "iso_code": "NG-LA",
"country_code": "NG",
"coordinates": { "lat": 6.5244, "lng": 3.3792 },
"postal_prefix": "100", "lga_count": 20
}],
"meta": { "total": 6 }
}
Accepts slug, state name, or ISO 3166-2:NG code. Case-insensitive.
GET /api/v1/states/lagos GET /api/v1/states/Lagos # also works GET /api/v1/states/NG-LA # also works
GET /api/v1/states/rivers/lgas
{
"status": "success",
"data": [{
"name": "Port Harcourt", "slug": "port-harcourt",
"state": "Rivers",
"coordinates": { "lat": 4.8156, "lng": 7.0498 },
"postal_code": "500001"
}],
"meta": {
"state": { "name": "Rivers", "capital": "Port Harcourt" },
"total": 23
}
}
Returns geographic bounding box and center point. Use to fly a map camera to show a full state.
GET /api/v1/states/lagos/bounds
{
"data": {
"state": "Lagos",
"south": 6.3934, "north": 6.7022,
"west": 3.0982, "east": 3.7297,
"center": { "lat": 6.5244, "lng": 3.3792 }
}
}
| Param | Type | Required | Description |
|---|---|---|---|
| state | string | optional | Filter by state slug or name |
GET /api/v1/lgas?state=kano
| Param | Type | Required | Description |
|---|---|---|---|
| q | string | required | Search term, min 2 chars |
| state | string | optional | Scope to a single state |
GET /api/v1/lgas/search?q=aba&state=abia
GET /api/v1/lgas/ikeja/bounds
{
"data": {
"lga": "Ikeja", "state": "Lagos",
"south": 6.5518, "north": 6.6518,
"west": 3.3015, "east": 3.4015,
"center": { "lat": 6.6018, "lng": 3.3515 }
}
}
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| radius | float | optional | 50 | Radius in km (max 500) |
| limit | int | optional | 10 | Max results (max 50) |
GET /api/v1/lgas/ikeja/nearby-lgas?radius=30&limit=5
GET /api/v1/geo-zones
{
"data": [{
"zone": "NC", "zone_full": "North Central",
"state_count": 7, "states": [ ... ]
}],
"meta": { "total_zones": 6 }
}
Accepts short form (SW) or full name (South West).
GET /api/v1/geo-zones/SW/states
| Param | Type | Required | Description |
|---|---|---|---|
| code | string | required | NIPOST code (3–10 digits). Tries exact LGA match, falls back to state prefix. |
GET /api/v1/postal-codes/lookup?code=100283
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| lat | float | required | — | Latitude |
| lng | float | required | — | Longitude |
| radius | float | optional | 100 | km, max 500 |
| limit | int | optional | 10 | max 50 |
| type | string | optional | lga | lga or state |
GET /api/v1/nearby?lat=6.45&lng=3.39&radius=30&limit=5
Calls Nominatim (OpenStreetMap) + enriches with local LGA/state from DB. Cached 24h. Falls back to local DB if Nominatim is down.
| Param | Type | Required |
|---|---|---|
| lat | float | required |
| lng | float | required |
GET /api/v1/geocode/reverse?lat=6.45&lng=3.39
{
"data": {
"full_address": "Allen Avenue, Ikeja, Lagos, Nigeria",
"coordinates": { "lat": 6.45, "lng": 3.39 },
"address_parts": {
"road": "Allen Avenue", "suburb": "Ikeja",
"city": "Lagos", "postcode": "100281"
},
"lga": { "name": "Ikeja", "postal_code": "100281" },
"state": { "name": "Lagos", "geo_zone": "SW" },
"source": "nominatim+local"
}
}
Queries Nominatim constrained to Nigeria. Returns short names for dropdown display. Cached 1h.
| Param | Type | Required | Description |
|---|---|---|---|
| q | string | required | Min 3 chars |
| limit | int | optional | Max 10, default 8 |
GET /api/v1/geocode/search?q=Victoria+Island+Lagos&limit=5
No external API calls — purely local Haversine match. No rate limit. Use for bulk GPS log processing or analytics pipelines.
POST /api/v1/geocode/batch
Content-Type: application/json
{
"points": [
{ "lat": 6.45, "lng": 3.39 },
{ "lat": 9.07, "lng": 7.39 }
]
}
// Each returns: lga, state, distance_to_lga_center_km
GET /api/v1/geocode/validate?lat=6.45&lng=3.39
{
"data": {
"lat": 6.45, "lng": 3.39,
"valid": true, "in_nigeria": true,
"nearest_lga": {
"name": "Ikeja", "state": "Lagos", "distance_km": 2.3
}
}
}
Powered by OSRM (free, no key). Returns Leaflet-ready polyline. Falls back to Haversine if OSRM unavailable. Cached 30min.
| Param | Type | Required | Description |
|---|---|---|---|
| from_lat / from_lng | float | required | Origin coordinates |
| to_lat / to_lng | float | required | Destination coordinates |
| profile | string | optional | driving | walking | cycling |
GET /api/v1/route?from_lat=6.45&from_lng=3.39&to_lat=9.07&to_lng=7.39
{
"data": {
"distance_km": 487.3,
"duration_minutes": 365,
"duration_text": "6 hr 5 mins",
"profile": "driving",
"polyline": [[6.45, 3.39], [6.48, 3.41], ...],
"steps": [
{ "instruction": "turn right", "name": "Allen Avenue",
"distance_km": 0.8, "duration_min": 3 }
],
"source": "osrm"
}
}
| Param | Type | Required | Description |
|---|---|---|---|
| from | string | required | Origin LGA slug |
| to | string | required | Destination LGA slug |
| profile | string | optional | driving | walking | cycling |
GET /api/v1/route/lgas?from=ikeja&to=lekki&profile=driving
Pure Haversine calculation. Includes rough duration estimate at 40km/h. Zero latency. Use for eligibility checks ("is this within 20km?").
GET /api/v1/distance?from_lat=6.45&from_lng=3.39&to_lat=9.07&to_lng=7.39
{
"data": {
"distance_km": 418.2, "duration_minutes": 627,
"duration_text": "10 hr 27 mins",
"note": "Straight-line estimate. Road distance will be longer.",
"source": "haversine"
}
}
Up to 10 origins × 10 destinations = 100 pairs. Results sorted by distance. Use for nearest-warehouse, nearest-agent, or coverage analysis.
POST /api/v1/distance/matrix
{
"origins": [
{ "lat": 6.45, "lng": 3.39, "label": "Warehouse Lagos" }
],
"destinations": [
{ "lat": 9.07, "lng": 7.39, "label": "Customer Abuja" },
{ "lat": 4.81, "lng": 7.04, "label": "Customer PH" }
]
}
// Returns all pairs sorted by distance_km
Integration Guide
Ready-made snippets for the most common Nigerian dev stacks.
const BASE = 'https://nigeria.jamiuadewaleyusuf.com/api/v1'; // State + LGA cascade const states = await fetch(`${BASE}/states`).then(r => r.json()); const { data: lgas } = await fetch(`${BASE}/states/lagos/lgas`).then(r => r.json()); // Reverse geocode (replaces Google Geocoding API) const geo = await fetch(`${BASE}/geocode/reverse?lat=6.45&lng=3.39`).then(r => r.json()); // Address autocomplete (replaces Google Places) const places = await fetch(`${BASE}/geocode/search?q=Victoria+Island&limit=5`).then(r => r.json()); // Road route + polyline (replaces Google Directions) const route = await fetch( `${BASE}/route?from_lat=6.45&from_lng=3.39&to_lat=9.07&to_lng=7.39` ).then(r => r.json()); // Nearby LGAs const nearby = await fetch(`${BASE}/nearby?lat=6.45&lng=3.39&radius=50`).then(r => r.json()); // Postal code lookup const location = await fetch(`${BASE}/postal-codes/lookup?code=100283`).then(r => r.json());
<?php
use Illuminate\Support\Facades\Http;
class NigeriaLocationService
{
private string $base = 'https://nigeria.jamiuadewaleyusuf.com/api/v1';
public function states(?string $geoZone = null): array
{
$params = $geoZone ? ['geo_zone' => $geoZone] : [];
return Http::get("{$this->base}/states", $params)->json('data');
}
public function lgasByState(string $state): array
{
return Http::get("{$this->base}/states/{$state}/lgas")->json('data');
}
public function reverseGeocode(float $lat, float $lng): ?array
{
return Http::get("{$this->base}/geocode/reverse", compact('lat', 'lng'))->json('data');
}
public function search(string $query, int $limit = 8): array
{
return Http::get("{$this->base}/geocode/search", ['q' => $query, 'limit' => $limit])->json('data');
}
public function route(float $fromLat, float $fromLng, float $toLat, float $toLng): array
{
return Http::get("{$this->base}/route", [
'from_lat' => $fromLat, 'from_lng' => $fromLng,
'to_lat' => $toLat, 'to_lng' => $toLng,
])->json('data');
}
public function nearby(float $lat, float $lng, int $radius = 100): array
{
return Http::get("{$this->base}/nearby", compact('lat', 'lng', 'radius'))->json('data');
}
public function lookupPostal(string $code): ?array
{
$resp = Http::get("{$this->base}/postal-codes/lookup", ['code' => $code])->json();
return $resp['status'] === 'success' ? $resp['data'] : null;
}
}
import requests BASE = "https://nigeria.jamiuadewaleyusuf.com/api/v1" # States by zone states = requests.get(f"{BASE}/states", params={"geo_zone": "SW"}).json()["data"] # LGAs for a state lgas = requests.get(f"{BASE}/states/lagos/lgas").json()["data"] # Reverse geocode geo = requests.get(f"{BASE}/geocode/reverse", params={"lat": 6.45, "lng": 3.39}).json()["data"] # Address search places = requests.get(f"{BASE}/geocode/search", params={"q": "Lekki Lagos", "limit": 5}).json()["data"] # Routing route = requests.get(f"{BASE}/route", params={ "from_lat": 6.45, "from_lng": 3.39, "to_lat": 9.07, "to_lng": 7.39 }).json()["data"] # Distance matrix (POST) matrix = requests.post(f"{BASE}/distance/matrix", json={ "origins": [{"lat": 6.45, "lng": 3.39, "label": "Lagos"}], "destinations": [{"lat": 9.07, "lng": 7.39, "label": "Abuja"}] }).json()["data"]
import 'dart:convert';
import 'package:http/http.dart' as http;
const _base = 'https://nigeria.jamiuadewaleyusuf.com/api/v1';
Future<List> getStates() async {
final res = await http.get(Uri.parse('$_base/states'));
return jsonDecode(res.body)['data'];
}
Future<List> getLgas(String state) async {
final res = await http.get(Uri.parse('$_base/states/$state/lgas'));
return jsonDecode(res.body)['data'];
}
Future<Map> reverseGeocode(double lat, double lng) async {
final uri = Uri.parse('$_base/geocode/reverse').replace(
queryParameters: {'lat': '$lat', 'lng': '$lng'}
);
final res = await http.get(uri);
return jsonDecode(res.body)['data'];
}
Future<Map> getRoute(double fromLat, double fromLng, double toLat, double toLng) async {
final uri = Uri.parse('$_base/route').replace(queryParameters: {
'from_lat': '$fromLat', 'from_lng': '$fromLng',
'to_lat': '$toLat', 'to_lng': '$toLng',
});
final res = await http.get(uri);
return jsonDecode(res.body)['data'];
}
import { useState, useEffect, useCallback } from 'react';
const BASE = 'https://nigeria.jamiuadewaleyusuf.com/api/v1';
// State → LGA cascade hook
export function useNigeriaLocation() {
const [states, setStates] = useState([]);
const [lgas, setLgas] = useState([]);
const [selected, setSelected] = useState('');
useEffect(() => {
fetch(`${BASE}/states`).then(r => r.json()).then(d => setStates(d.data));
}, []);
useEffect(() => {
if (!selected) return setLgas([]);
fetch(`${BASE}/states/${selected}/lgas`)
.then(r => r.json()).then(d => setLgas(d.data));
}, [selected]);
return { states, lgas, setSelected };
}
// Address autocomplete hook
export function useAddressSearch() {
const [results, setResults] = useState([]);
const search = useCallback(async (q) => {
if (q.length < 3) return setResults([]);
const data = await fetch(`${BASE}/geocode/search?q=${encodeURIComponent(q)}&limit=6`)
.then(r => r.json());
setResults(data.data || []);
}, []);
return { results, search };
}
# All states curl https://nigeria.jamiuadewaleyusuf.com/api/v1/states # States by geo zone curl "https://nigeria.jamiuadewaleyusuf.com/api/v1/states?geo_zone=SW" # LGAs for a state curl https://nigeria.jamiuadewaleyusuf.com/api/v1/states/lagos/lgas # Reverse geocode curl "https://nigeria.jamiuadewaleyusuf.com/api/v1/geocode/reverse?lat=6.45&lng=3.39" # Address autocomplete curl "https://nigeria.jamiuadewaleyusuf.com/api/v1/geocode/search?q=Victoria+Island" # Road route curl "https://nigeria.jamiuadewaleyusuf.com/api/v1/route?from_lat=6.45&from_lng=3.39&to_lat=9.07&to_lng=7.39" # LGA-to-LGA route curl "https://nigeria.jamiuadewaleyusuf.com/api/v1/route/lgas?from=ikeja&to=lekki" # Nearby (Abuja) curl "https://nigeria.jamiuadewaleyusuf.com/api/v1/nearby?lat=9.0765&lng=7.3986&radius=50" # Postal code lookup curl "https://nigeria.jamiuadewaleyusuf.com/api/v1/postal-codes/lookup?code=900001" # Validate coordinates curl "https://nigeria.jamiuadewaleyusuf.com/api/v1/geocode/validate?lat=6.45&lng=3.39" # Distance matrix (POST) curl -X POST https://nigeria.jamiuadewaleyusuf.com/api/v1/distance/matrix \ -H "Content-Type: application/json" \ -d '{"origins":[{"lat":6.45,"lng":3.39,"label":"Lagos"}],"destinations":[{"lat":9.07,"lng":7.39,"label":"Abuja"}]}'
Use Cases
Every endpoint was designed around a specific recurring pain in Nigerian product development.
Scope & Limits
Understanding the boundary is as important as understanding the capability. This is infrastructure, not a product — know where it ends.
departure_time=now for live traffic — no free alternative exists.| What you need | Free solution | Paid if you need more |
|---|---|---|
| State / LGA dropdown | This API /states, /lgas | — |
| Postal code | This API /postal-codes/lookup | — |
| Geopolitical zone grouping | This API /geo-zones | — |
| Coordinates for state / LGA | This API in every response | — |
| Proximity / nearest LGA | This API /nearby, /nearby-lgas | — |
| Map rendering | Free Leaflet.js + OpenStreetMap | Google Maps SDK |
| Address autocomplete | This API /geocode/search | Google Places API |
| Reverse geocode (lat → address) | This API /geocode/reverse | Google Geocoding API |
| Road route + polyline | This API /route via OSRM | Google Directions API |
| Distance + duration estimate | This API /distance | Google Directions API |
| Real-time traffic | Not available free | Google Directions API |
| Street View | Not available free | Google Street View |
| Business / POI search | Limited OSM Overpass API | Google Places API |
| High-res satellite imagery | Non-commercial Esri | Google Maps / Mapbox |
| Consumer voice navigation | Use Google Maps / Waze | Google Maps SDK |
| High-volume geocoding (1000s/min) | Self-host Nominatim | Google / Mapbox |
Self-hosting for production scale
For high traffic, self-host Nominatim (free geocoding, Nigeria OSM ~120MB, ~2hr import) and OSRM (free routing, ~30min processing) on your own VPS. Then update GeoService.php to point to http://localhost:8080 and http://localhost:5000 respectively. After that: zero external dependencies, unlimited throughput.
# Self-host Nominatim (geocoding) docker run -e PBF_URL=https://download.geofabrik.de/africa/nigeria-latest.osm.pbf \ -p 8080:8080 mediagis/nominatim:4.4 # Self-host OSRM (routing) wget https://download.geofabrik.de/africa/nigeria-latest.osm.pbf docker run -t -v $(pwd):/data osrm/osrm-backend osrm-extract -p /opt/car.lua /data/nigeria-latest.osm.pbf docker run -t -i -p 5000:5000 -v $(pwd):/data osrm/osrm-backend osrm-routed --algorithm mld /data/nigeria-latest.osrm
FAQ
php artisan migrate && php artisan db:seed, and you have a private instance with no external dependencies. This is the recommended approach for teams with strict data residency requirements or high traffic needs.
/api/v1/states on page load to populate the state dropdown. When a user selects a state, fetch /api/v1/states/{slug}/lgas to populate the LGA dropdown. Both responses are fast enough for real-time use without client-side caching. See the React hook in the Integration Guide for a ready-made implementation.
type=state to find nearest states instead of LGAs. For pre-built convenience, use /lgas/{slug}/nearby-lgas to find all LGAs surrounding a known LGA without needing raw coordinates.