Swagger UI OpenAPI schema

Mobile API (v1)

Base path: /mobile-api/v1/

OpenAPI docs: - Schema JSON: /mobile-api/schema/ - Swagger UI: /mobile-api/docs/


Authentication & Identity

The API uses a persistent client identity model — no passwords, no OAuth tokens.

For returning users, the PIN-based login flow allows restoring a session on a new device (see Login).


Endpoints

Health

GET /mobile-api/v1/health/

No authentication required.

Response 200:

{
  "status": "ok",
  "service": "offmap-mobile-api",
  "version": "v1"
}

Connection

GET /mobile-api/v1/connection/

No authentication required. No client ID needed and no cookie is set. This is the API version of the web page at /my-connection/.

Returns the caller's IP address and connection metadata as the server sees it: the public IP, reverse DNS, ISP/ASN, approximate location and the request headers that matter for diagnostics.

Query parameters:

Name Type Default Description
lookup bool true Set to false (or 0/no) to skip the reverse DNS and ISP/geo lookups. reverse_dns, provider and location are then null, and the response returns faster.

Response 200:

{
  "ip": "203.0.113.42",
  "ip_version": 4,
  "ip_scope": "public",
  "remote_addr": "127.0.0.1",
  "reverse_dns": "cpe-203-0-113-42.example.net.au",
  "provider": {
    "isp": "Example Broadband",
    "org": "Example Broadband Pty Ltd",
    "asn": "AS64500 Example Broadband Pty Ltd",
    "as_name": "EXAMPLE-AS",
    "mobile": false,
    "proxy": false,
    "hosting": false
  },
  "location": {
    "city": "Brisbane",
    "region": "Queensland",
    "postal_code": "4000",
    "country": "Australia",
    "country_code": "AU",
    "lat": -27.4698,
    "lon": 153.0251,
    "timezone": "Australia/Brisbane"
  },
  "request": {
    "scheme": "https",
    "host": "mapgrid.com.au",
    "server_protocol": "HTTP/1.1"
  },
  "headers": {
    "User-Agent": "MapGrid/1.0 (iOS)",
    "X-Forwarded-For": "203.0.113.42"
  }
}

Fields:

Field Description
ip Caller IP. The server checks CF-Connecting-IP, then X-Real-IP, then the first entry of X-Forwarded-For, and finally uses the socket address.
ip_version 4, 6, or null if the IP could not be parsed.
ip_scope public, private, loopback, reserved or invalid.
remote_addr The address of the direct socket peer (normally the reverse proxy).
reverse_dns PTR hostname for ip, or null if there is none or the 2 s timeout is reached.
provider ISP, organisation and ASN. mobile, proxy (VPN/proxy/Tor) and hosting (datacenter) are flags. null for non-public IPs, when lookup=false, or if the lookup fails.
location Approximate location of the IP. This is usually the ISP's location, not the device's. null in the same cases as provider.
request Scheme, host and HTTP protocol of the request as received by Django.
headers The request headers present from this set: forwarding headers (X-Forwarded-For, X-Real-IP, CF-Connecting-IP, X-Forwarded-Proto, Via, Forwarded), User-Agent, Accept-Language, Accept-Encoding, DNT, Sec-GPC, Sec-CH-UA* and Save-Data.

Notes: - ISP and location data come from ip-api.com (free tier, about 45 lookups per minute per server). If the lookup fails, provider and location come back as null and the response is still 200. - Because the IP is taken from forwarding headers, a client can spoof the reported value. Use this for diagnostics, not for access control. - The server cannot see the device's LAN IP, its DNS resolver or its IPv6 address if the request arrived over IPv4. To get those on mobile, query them on the device (or call https://api6.ipify.org for IPv6). - Responses are sent with Cache-Control: no-store.

Example:

curl https://mapgrid.com.au/mobile-api/v1/connection/
curl "https://mapgrid.com.au/mobile-api/v1/connection/?lookup=false"

Profile

Manage the user's display name and PIN. A profile is required before using tracking groups or location sharing.

Get Profile

GET /mobile-api/v1/profile/

Response 200:

{
  "profile": {
    "name": "Rob",
    "pin": "48201735"
  }
}

Returns {"profile": null} if no profile exists for the current client.

Set Name (Create or Update)

POST /mobile-api/v1/profile/

Request:

{
  "name": "Rob"
}

Response 200:

{
  "profile": {
    "name": "Rob",
    "pin": "48201735"
  }
}

Login

Restore a session on a new device by providing your 8-digit PIN. No authentication required.

POST /mobile-api/v1/login/

Request:

{
  "pin": "48201735"
}

Response 200:

{
  "profile": {
    "name": "Rob",
    "pin": "48201735"
  },
  "logged_in": true
}

The response sets the offmap_client_id cookie to the existing client ID associated with that PIN.

Error 404:

{
  "error": "PIN not found."
}

Logout

Reset the current session to a fresh client ID. No authentication required.

POST /mobile-api/v1/logout/

Response 200:

{
  "logged_out": true
}

The response sets the offmap_client_id cookie to a new UUID.


Home Location

Get Home

GET /mobile-api/v1/home/

Response 200:

{
  "home": {
    "lat": -33.8688,
    "lon": 151.2093,
    "zoom": 15,
    "label": "Home"
  }
}

Returns {"home": null} if not set.

Save / Update Home

PUT /mobile-api/v1/home/

Request:

{
  "lat": -33.8688,
  "lon": 151.2093,
  "zoom": 15,
  "label": "Home"
}
Field Type Required Notes
lat float yes -90 to 90
lon float yes -180 to 180
zoom int no 0–18, default 15
label string no max 120 chars, default "Home"

Delete Home

DELETE /mobile-api/v1/home/

Response 200:

{
  "removed": true
}

Trip

A client has at most one active trip containing ordered stops (start, via, end).

Get Trip

GET /mobile-api/v1/trip/

Response 200:

{
  "trip": {
    "id": 1,
    "name": "Trip",
    "stops": [
      {
        "id": 10,
        "stop_type": "start",
        "order": 0,
        "label": "Sydney CBD",
        "lat": -33.8688,
        "lon": 151.2093,
        "source": "manual"
      },
      {
        "id": 11,
        "stop_type": "via",
        "order": 1,
        "label": "Grid 55HCU 1234 5678",
        "lat": -23.697,
        "lon": 133.8807,
        "source": "grid"
      }
    ]
  }
}

Add Stop

POST /mobile-api/v1/trip/

Request:

{
  "stop_type": "via",
  "label": "Grid 55HCU 1234 5678",
  "lat": -23.697,
  "lon": 133.8807,
  "source": "grid"
}
Field Type Required Notes
stop_type string yes start, via, or end
label string yes max 180 chars
lat float yes -90 to 90
lon float yes -180 to 180
source string no max 16 chars, default "manual"

Ordering rules: - start replaces any existing start and is placed first. - end replaces any existing end and is placed last. - via is inserted before any existing end stop.

Response 201 returns the full trip object.

Clear All Stops

DELETE /mobile-api/v1/trip/

Response 200:

{
  "trip": {
    "name": "Trip",
    "stops": []
  }
}

Delete Single Stop

DELETE /mobile-api/v1/trip/stops/{stop_id}/

Removes the stop and re-indexes remaining stop order. Returns the updated trip object.

Calculate Route

GET /mobile-api/v1/trip/route/

Calculates a route through all stops in the current trip (in order). Requires at least 2 stops.

The server uses either OSRM or GraphHopper as the routing backend (configured server-side via ROUTING_PROVIDER).

Response 200 (route found):

{
  "route": {
    "coordinates": [[-33.8688, 151.2093], [-33.75, 151.1], ...],
    "distance_km": 42.5,
    "duration_min": 35.2,
    "provider": "osrm"
  }
}

Response 200 (no trip or fewer than 2 stops):

{
  "route": null
}

Error 502 (routing backend unreachable or returned no result):

{
  "error": "OSRM returned no route."
}

Notes: - coordinates is an array of [lat, lon] pairs representing the full route polyline. - distance_km is the total route distance in kilometres. - duration_min is the estimated travel time in minutes. - provider indicates which routing engine was used (osrm or graphhopper).


Search (Geocoding)

Forward geocoding — search for a place by name and get coordinates. Proxied to the self-hosted Nominatim instance. No authentication required.

Search Places

GET /mobile-api/v1/search/?q={query}

Param Type Required Notes
q string yes Place name or address to search for
limit int no Max results (default 8)
countrycodes string no Comma-separated ISO 3166-1 codes, e.g. au

Response 200:

[
  {
    "place_id": 4871348,
    "osm_type": "relation",
    "osm_id": 5750005,
    "lat": "-33.8698439",
    "lon": "151.2082848",
    "category": "place",
    "type": "city",
    "place_rank": 16,
    "importance": 0.187,
    "addresstype": "city",
    "name": "Sydney",
    "display_name": "Sydney, Council of the City of Sydney, New South Wales, Australia",
    "boundingbox": ["-34.26", "-33.36", "150.26", "151.34"],
    "address": {
      "city": "Sydney",
      "municipality": "Council of the City of Sydney",
      "state": "New South Wales",
      "country": "Australia",
      "country_code": "au"
    }
  }
]

Error 502 if the geocoding backend is unreachable.

Reverse Geocode

GET /mobile-api/v1/reverse-geocode/?lat={lat}&lon={lon}

Param Type Required Notes
lat string yes Latitude
lon string yes Longitude

Response 200:

{
  "place_id": 4936616,
  "osm_type": "relation",
  "osm_id": 5729534,
  "lat": "-33.867952",
  "lon": "151.210134",
  "display_name": "Sydney, Council of the City of Sydney, New South Wales, 2000, Australia",
  "address": {
    "suburb": "Sydney",
    "municipality": "Council of the City of Sydney",
    "state": "New South Wales",
    "postcode": "2000",
    "country": "Australia",
    "country_code": "au"
  }
}

Error 502 if the geocoding backend is unreachable.


Share Location

Generate a shareable URL pointing to the web map at specific coordinates. No authentication required.

POST /mobile-api/v1/share-location/

Request:

{
  "lat": -23.697,
  "lon": 133.8807,
  "label": "50JLK 8024 8868",
  "zoom": 14
}
Field Type Required Notes
lat float yes -90 to 90
lon float yes -180 to 180
label string no max 180 chars, default "Shared Location"
zoom int no 0–18, default 14

Response 200:

{
  "url": "https://offmap.example.com/?lat=-23.697&lon=133.8807&zoom=14&label=50JLK+8024+8868",
  "lat": -23.697,
  "lon": 133.8807,
  "label": "50JLK 8024 8868",
  "zoom": 14
}

Map Fragments (Offline Download)

Estimate

POST /mobile-api/v1/map-fragments/estimate/

Estimate tile count and approximate download size before committing.

Request:

{
  "min_lat": -34.2,
  "min_lon": 150.8,
  "max_lat": -33.7,
  "max_lon": 151.4,
  "min_zoom": 12,
  "max_zoom": 15
}

Response 200:

{
  "tile_count": 1260,
  "estimated_size_mb": 30.24,
  "warning": "Estimate assumes average compressed tile size."
}

Download

POST /mobile-api/v1/map-fragments/download/

Generates and returns a ZIP file containing tiles for the given bounding box and zoom range.

Request body is identical to the estimate endpoint.

Response headers: - Content-Type: application/zip - Content-Disposition: attachment; filename="fragment_z12-15.zip" - X-Included-Tiles: 1245 - X-Missing-Tiles: 15

The ZIP contains: - Tiles at {zoom}/{x}/{y}.png - manifest.json with download metadata:

{
  "tile_url_template": "https://tiles.example.com/{z}/{x}/{y}.png",
  "bbox": {
    "min_lat": -34.2,
    "min_lon": 150.8,
    "max_lat": -33.7,
    "max_lon": 151.4
  },
  "min_zoom": 12,
  "max_zoom": 15,
  "tile_count": 1260,
  "included_tiles": 1245,
  "missing_tiles": 15
}
Field Type Required Notes
min_lat float yes -90 to 90
min_lon float yes -180 to 180
max_lat float yes must be > min_lat
max_lon float yes must be > min_lon
min_zoom int yes 0–18
max_zoom int yes must be >= min_zoom

Error 413 if tile count exceeds MOBILE_API_MAX_FRAGMENT_TILES:

{
  "error": "Requested fragment exceeds max tile count.",
  "tile_count": 50000,
  "max_allowed": 10000
}

Tracking Groups

Tracking groups allow multiple users to share real-time locations with each other. The group creator is the owner who approves or rejects join requests.

List My Groups

GET /mobile-api/v1/tracking-groups/

Returns groups created by the current client.

Response 200:

{
  "groups": [
    {
      "id": 1,
      "name": "Family Trip",
      "pin": "91827364"
    }
  ]
}

Create Group

POST /mobile-api/v1/tracking-groups/

Request:

{
  "name": "Family Trip"
}

Response 200:

{
  "group": {
    "id": 1,
    "name": "Family Trip",
    "pin": "91827364"
  }
}

A unique 8-digit PIN is generated for the group. Share this PIN with others so they can join.

Join Group

POST /mobile-api/v1/tracking-groups/join/

No authentication required (uses PINs for identity).

Request:

{
  "user_pin": "48201735",
  "group_pin": "91827364"
}

Response 200:

{
  "membership": {
    "id": 5,
    "group_id": 1,
    "group_name": "Family Trip",
    "profile_name": "Rob",
    "profile_pin": "48201735",
    "status": "pending"
  },
  "message": "Join request submitted. Waiting for group owner approval."
}

Errors: - 404 if user PIN or group PIN not found. - Re-submitting after rejection resets status to pending.

List Group Members

GET /mobile-api/v1/tracking-groups/members/?group_id={id}

Response 200:

{
  "group": {
    "id": 1,
    "name": "Family Trip",
    "pin": "91827364"
  },
  "is_owner": true,
  "members": [
    {
      "id": 5,
      "group_id": 1,
      "group_name": "Family Trip",
      "profile_name": "Rob",
      "profile_pin": "48201735",
      "status": "approved"
    }
  ]
}

Approve Membership

POST /mobile-api/v1/tracking-groups/approve/

Owner-only. Changes a membership status to approved.

Request:

{
  "membership_id": 5
}

Response 200:

{
  "membership": {
    "id": 5,
    "group_id": 1,
    "group_name": "Family Trip",
    "profile_name": "Rob",
    "profile_pin": "48201735",
    "status": "approved"
  }
}

Error 403 if the caller is not the group owner.

Reject Membership

POST /mobile-api/v1/tracking-groups/reject/

Owner-only. Changes a membership status to rejected.

Request:

{
  "membership_id": 5
}

Response and error behaviour identical to approve.


Location Updates

Post My Location

POST /mobile-api/v1/location/update/

Updates the current user's location (requires a profile).

Request:

{
  "lat": -33.8688,
  "lon": 151.2093
}

Response 200:

{
  "updated": true
}

Error 400 if no profile exists:

{
  "error": "Set your name first to enable location sharing."
}

Get Group Locations

GET /mobile-api/v1/tracking-groups/locations/?group_id={id}

Returns the latest location of all approved members and the group owner.

Requires the caller to be either the group owner or an approved member.

Response 200:

{
  "group": {
    "id": 1,
    "name": "Family Trip",
    "pin": "91827364"
  },
  "locations": [
    {
      "name": "Rob",
      "pin": "48201735",
      "lat": -33.8688,
      "lon": 151.2093,
      "updated_at": "2026-07-26T10:30:00+00:00"
    }
  ]
}

Error 403 if the caller is not the owner or an approved member.


Error Format

All error responses use a consistent shape:

{
  "error": "Human-readable error message."
}

Validation errors (from invalid request bodies) return 400 with field-level detail from Django REST Framework:

{
  "lat": ["Ensure this value is greater than or equal to -90."],
  "stop_type": ["\"invalid\" is not a valid choice."]
}

Mobile Integration Notes

  1. Persist X-Client-ID — store the UUID in app secure storage and send it on every request.
  2. Set up a profile early — call POST /profile/ with a display name. The returned PIN is the user's identity for login and group features.
  3. PIN login for device transfer — use POST /login/ with the 8-digit PIN to restore a session on a new device.
  4. Estimate before download — always call the estimate endpoint before triggering a tile download to show the user expected size.
  5. Parse manifest.json — after unzipping a map fragment, read the manifest for included/missing tile counts.
  6. Polling for locations — call GET /tracking-groups/locations/?group_id=X at a reasonable interval (e.g. every 30s) to update group member positions on the map.
  7. Group join flow — user shares their user PIN + the group PIN. After calling join, the request is pending until the group owner approves via the approve endpoint.