Mobile Integration
This walks through the shape of a real navigation-style mobile app built on AfriHex — search, turn-by-turn directions, an account, and live map overlays. The endpoints below are the same ones covered in Addresses & Geocoding, Hex Addressing, and Routing & Navigation; this guide is the mobile-specific path through them, plus the pieces that only make sense once you're integrating a device app — per-user tokens, offline-friendly public endpoints, and a couple of gotchas that only show up on a phone.
authenticate → search & geocode → directions → live map layers (no key)
1. Authenticate — mint a token per user, not one shared key
There are two ways to get a value for the X-API-Key header:
POST /v2/self/signup— one static key for your whole application. Good for a backend service calling AfriHex on its own behalf.POST /v2/auth/loginorPOST /v2/auth/google/mobile— a personal token, one per signed-in user.
Use #2 for a mobile app. A static key baked into an app bundle is extractable from the compiled APK/IPA by anyone who bothers to look, and it's shared by every install — one leaked key exposes every user's quota at once, and you can't revoke a single compromised device without breaking everyone else. A per-user token is scoped to that one account: rotate or revoke it without touching anyone else.
curl -X POST "https://api.afrihex.com/v2/auth/login" \
-H "Content-Type: application/json" \
-d '{ "email": "user@example.com", "password": "…" }'
{
"success": true,
"data": {
"token": "734ee12998fdca3e2a8fd3e1feed18f4451670a4cc8ee139ab2913a2d8b07dc0",
"user": {
"id": 1, "name": "Ama Owusu", "email": "user@example.com",
"plan": "team", "daily_limit": 10000, "usage_today": 0,
"expires_at": "2027-05-06T11:31:44Z",
"api_key": "734ee129...",
"is_admin": false
}
}
}
data.token is an X-API-Key value — not a separate bearer/session
scheme. Send it in the same header as any other key, on every subsequent
call:
curl "https://api.afrihex.com/v2/me" -H "X-API-Key: 734ee12998fdca3e2a8fd3e1feed18f4451670a4cc8ee139ab2913a2d8b07dc0"
Store it in the platform's secure storage — Keychain on iOS, Keystore on
Android — never in plain preferences and never logged. POST /v2/auth/google/mobile
(body: { "id_token": "<google id token>" }) returns the same { token, user }
shape for Google sign-in; verify the token's audience matches your app's
client ID first, or it's rejected with INVALID_GOOGLE_AUDIENCE.
| Endpoint | Purpose |
|---|---|
POST /v2/auth/login | Email/password sign-in |
POST /v2/auth/google/mobile | Google sign-in, verified server-side (no redirect) |
POST /v2/auth/forgot-password | Email a one-time, 30-minute password reset token |
POST /v2/auth/reset-password | Reset the password with the emailed token |
POST /v2/me/password | Attach a password to an OAuth-only account, or change one |
GET /v2/me | Current user + plan — expires_at is subscription expiry, not the session; never force-logout on it |
POST /v2/me/rotate | Rotate the token (old one is revoked immediately) |
POST /v2/auth/forgot-password always returns the same generic response,
whether or not the email exists — don't use it to signal account existence in
your UI. The emailed reset link is a deep link
({RESET_LINK_BASE_URL}/reset-password?token=…) you can register to open
directly in the app.
See Authentication for the full reference, including plan tiers and rate limits.
2. Search & geocode
Type-ahead, place search, reverse geocoding, and nearby-places are covered in Addresses & Geocoding — start there for the base geocoding calls. Two endpoints are specifically mobile-app pieces and aren't documented elsewhere:
Resolve a hex code to its precise pin (authenticated)
GET /v2/hexcode/{code} (see Hex Addressing) decodes a hex purely
from H3 grid math — it always returns the cell centroid. For placing a
navigation pin, use GET /v2/address/resolve?hex=… instead, which returns
the best-known point for that address:
curl "https://api.afrihex.com/v2/address/resolve?hex=AF-GH-7-0GXTJJB0ZZZZZ" \
-H "X-API-Key: $AFRIHEX_API_KEY"
{
"success": true,
"data": {
"hex": "AF-GH-7-0GXTJJB0ZZZZZ",
"point": { "lng": -0.1867, "lat": 5.604 },
"precision": "structure",
"confidence": 85,
"label": "main gate",
"last_seen_at": "2026-08-01T10:00:00Z"
}
}
There's no pin field — the coordinate is data.point. Trust it using
precision + confidence together:
| Precision | Meaning | Typical confidence |
|---|---|---|
structure | A confirmed gate/door from a field survey. Carries label and last_seen_at | 75–90 |
building_edge | Snapped to the road-facing edge of the nearest building footprint (the likely gate side) | 65 |
hex_centroid | No footprint or survey data — plain cell-centre fallback | 50 |
A hex_centroid result is a guess; consider showing a wider "approximate
location" pin state in the UI rather than dropping a precise marker.
Recent & frequent searches (authenticated)
Per-user search history that follows the account across devices — good for a "recent" list under the search bar:
# Save a successful search (idempotent per user+query — re-searching bumps
# search_count and recency instead of creating a duplicate)
curl -X POST "https://api.afrihex.com/v2/me/recent-searches" \
-H "X-API-Key: $AFRIHEX_API_KEY" -H "Content-Type: application/json" \
-d '{ "query": "Accra Mall", "result_type": "place", "result_ref": "GL1524944",
"display_name": "Accra Mall, Greater Accra", "lat": 5.6221843, "lng": -0.1729361 }'
# List them, most-used first
curl "https://api.afrihex.com/v2/me/recent-searches?limit=10" -H "X-API-Key: $AFRIHEX_API_KEY"
{
"success": true,
"data": {
"count": 1,
"searches": [
{
"id": 9, "query": "Accra Mall", "result_type": "place",
"result_ref": "GL1524944", "display_name": "Accra Mall, Greater Accra",
"lat": 5.6221843, "lng": -0.1729361,
"last_searched_at": "2026-08-09T16:35:05Z", "search_count": 3
}
]
}
}
| Endpoint | Purpose |
|---|---|
POST /v2/me/recent-searches | Save a search — result_type is gps, place, landmark, or poi |
GET /v2/me/recent-searches?limit=N | List, ranked by frequency then recency (default 20, max 50) |
DELETE /v2/me/recent-searches/{id} | Remove one entry — 404 NOT_FOUND if it's missing or not yours |
DELETE /v2/me/recent-searches | Clear the whole list (no body) |
3. Directions
The full routing contract — modes (including truck), flood-aware avoidance,
transit, landmark narration, isochrones — lives in
Routing & Navigation. For a mobile client specifically:
- No account yet?
POST /v2/route/publictakes the same body asPOST /v2/routebut needs noX-API-Key— useful for a "how far is this?" preview before someone has signed in. - Live tracking screen? Pass
lite: trueon/v2/route— polyline plus distance/duration/ETA only, no steps or alternatives, typically 80–90% smaller. Less data, less battery, on a screen that's re-rendering every few seconds anyway. - Turn-by-turn voice?
narration: "both"or"landmark"gives landmark-enriched instructions ("turn right at the GOIL station") in addition to the street-name form — closer to how people actually navigate where addressing is sparse. - A route thumbnail in a results list?
GET /v2/route/static(see below) renders a fixed 800×500 PNG preview with no key and no full turn-by-turn payload to parse — cheaper than loading/v2/routejust to show a distance card. - Riding transit? OTP answers a trip it can't serve with a plain walk
rather than an error, so check
no_transitandwalk_onlyonPOST /v2/route/transitbefore presenting a walk as a trotro/bus journey — don't infer "no service" from leg modes yourself.GET /v2/transit/departuresanswers the complementary "what can I board here?" question for a station board UI. Both are public — see No service, honestly and What leaves from here?. - Turn-by-turn navigation?
POST /v2/navigate/routeneeds noX-API-Keyeither — a route planned while logged out can be navigated all the way through without hitting a login wall on "Navigate".
4. Live map layers — no API key required
These endpoints take no X-API-Key at all, which matters for a mobile app in
two ways: you can render map overlays before a user has signed in, and
hitting them doesn't spend anyone's request quota.
| Endpoint | Returns | Cache-Control |
|---|---|---|
GET /v2/route/flood-zones | GeoJSON of curated flood-prone zones | max-age=600 |
GET /v2/weather/alerts | GeoJSON of active official GMet alerts | max-age=300 |
GET /v2/precipitation-forecast | GeoJSON point cloud, next-6h rain-ahead forecast | max-age=600 |
GET /v2/route/static?from=…&to=…&mode=… | Fixed 800×500 PNG route preview | max-age=3600 |
GET /v2/transit/departures?lat=…&lng=… | Boardable trotro/bus lines near a point | — |
POST /v2/navigation/arrival | 204 — records whether a nav session actually arrived | — |
POST /v2/route/public | Same as POST /v2/route, no key required | — |
Respect the Cache-Control headers on the client — these layers refresh on
the interval shown, so a map screen doesn't need to re-fetch them on every
open.
curl "https://api.afrihex.com/v2/weather/alerts"
{
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {
"event": "Rain/Wet Spell", "severity": "Moderate", "urgency": "Immediate",
"headline": "Weather Alert: Continuous Rain Over Southern Ghana",
"instruction": "Carry umbrellas. Localised flash floods are anticipated.",
"areas": ["Greater Accra"], "source": "GMet", "expires_at": "2026-06-21T15:00:00Z"
},
"geometry": { "type": "MultiPolygon", "coordinates": [ [[[-0.3, 5.5], [-0.1, 5.5], [-0.1, 5.7], [-0.3, 5.7], [-0.3, 5.5]]] ] }
}]
}
POST /v2/navigation/arrival is worth calling even for anonymous sessions —
it's the input to the learned-traffic model that powers eta_s on
/v2/route, and it fails open: missing or zero fields are silently dropped
with 204, never a 400. See
Navigation for the full field list.
5. Attribution — required if you display landmark data
Landmarks carry a source field naming the dataset they came from, and most
of those datasets require credit wherever the data is shown. This doesn't
have to sit on the map itself — an "About" or "Legal" screen is fine:
Place data © OpenStreetMap contributors (ODbL), © 2025 Foursquare Labs, Inc. (Apache 2.0), © Overture Maps Foundation (CDLA-Permissive 2.0), © GeoNames (CC BY 4.0).
Wikidata-sourced landmarks (source: "wikidata") are CC0 and need no credit.
The basemap tiles are OpenStreetMap-derived and render their own
"© OpenStreetMap" credit — that covers the tiles, not the place data above.
Errors & resilience
Every error follows { success: false, error: { code, message } }:
{ "success": false, "error": { "code": "MISSING_API_KEY", "message": "X-API-Key header is required" } }
{ "success": false, "error": { "code": "INVALID_API_KEY", "message": "invalid or revoked API key" } }
{ "success": false, "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "…" } }
See Responses for the full envelope and status-code reference. A few endpoints in this guide are deliberately fail-open — they return a success response with empty or default data rather than an error, so a flaky signal never blocks the screen it feeds:
POST /v2/navigation/arrival— malformed JSON is the only400; anything else silently drops with204.GET /v2/precipitation-forecast— returns an emptyFeatureCollectionrather than an error if the underlying data is unavailable.GET /v2/route/traffic— degrades to static time-of-day estimates rather than failing when live traffic data isn't available.
Treat these as best-effort enhancements in your UI, not as things to retry or surface an error for.
Next steps
- Authentication — the full key/token reference.
- Addresses & Geocoding and Hex Addressing — the base geocoding calls this guide builds on.
- Routing & Navigation — the full directions contract: modes, flood avoidance, transit, isochrones.
- Location Monitoring — if you're building a fintech/logistics app with background tracking rather than a consumer nav app.
- API Reference — every endpoint, browsable and testable.