Skip to main content

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:

  1. POST /v2/self/signup — one static key for your whole application. Good for a backend service calling AfriHex on its own behalf.
  2. POST /v2/auth/login or POST /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.

EndpointPurpose
POST /v2/auth/loginEmail/password sign-in
POST /v2/auth/google/mobileGoogle sign-in, verified server-side (no redirect)
POST /v2/auth/forgot-passwordEmail a one-time, 30-minute password reset token
POST /v2/auth/reset-passwordReset the password with the emailed token
POST /v2/me/passwordAttach a password to an OAuth-only account, or change one
GET /v2/meCurrent user + plan — expires_at is subscription expiry, not the session; never force-logout on it
POST /v2/me/rotateRotate 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:

PrecisionMeaningTypical confidence
structureA confirmed gate/door from a field survey. Carries label and last_seen_at75–90
building_edgeSnapped to the road-facing edge of the nearest building footprint (the likely gate side)65
hex_centroidNo footprint or survey data — plain cell-centre fallback50

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
}
]
}
}
EndpointPurpose
POST /v2/me/recent-searchesSave a search — result_type is gps, place, landmark, or poi
GET /v2/me/recent-searches?limit=NList, 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-searchesClear 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/public takes the same body as POST /v2/route but needs no X-API-Key — useful for a "how far is this?" preview before someone has signed in.
  • Live tracking screen? Pass lite: true on /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/route just 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_transit and walk_only on POST /v2/route/transit before presenting a walk as a trotro/bus journey — don't infer "no service" from leg modes yourself. GET /v2/transit/departures answers 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/route needs no X-API-Key either — 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.

EndpointReturnsCache-Control
GET /v2/route/flood-zonesGeoJSON of curated flood-prone zonesmax-age=600
GET /v2/weather/alertsGeoJSON of active official GMet alertsmax-age=300
GET /v2/precipitation-forecastGeoJSON point cloud, next-6h rain-ahead forecastmax-age=600
GET /v2/route/static?from=…&to=…&mode=…Fixed 800×500 PNG route previewmax-age=3600
GET /v2/transit/departures?lat=…&lng=…Boardable trotro/bus lines near a point—
POST /v2/navigation/arrival204 — records whether a nav session actually arrived—
POST /v2/route/publicSame 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 only 400; anything else silently drops with 204.
  • GET /v2/precipitation-forecast — returns an empty FeatureCollection rather 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​