Kotlin

Mapricot's API is plain HTTPS and JSON, so an Android app can call it with HttpURLConnection and org.json, which come with Android, and Kotlin coroutines. This page calls each API: geocoding, places, routing, and IP geolocation, and shows a map on the free tiles with MapLibre Native. The API code was tested with Kotlin 2.4 and kotlinx.coroutines 1.11. For Kotlin on a server, the Java page's HttpClient code works too.

Set up

Create a server key in the dashboard with only the products the app uses: Places for geocoding and places, Routing, or IP geolocation. Give it caps on requests per minute and per 30 days, as Getting started shows. An app sends no web origin, so a browser key is refused there with 403, and anyone who unpacks the app can read the key inside it. To keep the key out of the app entirely, send requests through your own backend instead. The map tiles need no key.

Put the key in ~/.gradle/gradle.properties, outside your project, so it stays out of your repository:

mapricotKey=mapricot_sk_…

Then build it into the app in your module's build.gradle.kts:

android {
    buildFeatures {
        buildConfig = true
    }
    defaultConfig {
        buildConfigField("String", "MAPRICOT_KEY", "\"${providers.gradleProperty("mapricotKey").get()}\"")
    }
}

The app needs the internet permission, in AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

Then add a function that sends the key with every request, retries when the API is busy, and throws on errors. It sends a POST when there's a body, runs the request off the main thread, and returns the response as a JSONObject:

import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject

class MapricotException(val status: Int, message: String) : IOException(message)

suspend fun mapricot(path: String, query: Map<String, Any> = emptyMap(), body: JSONObject? = null): JSONObject {
    val queryString = query.entries.joinToString("&") { (name, value) -> "$name=${URLEncoder.encode(value.toString(), "UTF-8")}" }
    val url = URL("https://api.mapricot.com$path" + if (queryString.isEmpty()) "" else "?$queryString")
    var attempt = 1
    while (true) {
        val (status, json) = withContext(Dispatchers.IO) {
            val connection = url.openConnection() as HttpURLConnection
            connection.connectTimeout = 10_000
            connection.readTimeout = 30_000
            connection.setRequestProperty("Authorization", "Bearer ${BuildConfig.MAPRICOT_KEY}")
            if (body != null) {
                connection.requestMethod = "POST"
                connection.doOutput = true
                connection.setRequestProperty("Content-Type", "application/json")
                connection.outputStream.use { it.write(body.toString().toByteArray()) }
            }
            val status = connection.responseCode
            val stream = if (status >= 400) connection.errorStream else connection.inputStream
            status to stream.bufferedReader().use { it.readText() }
        }
        if ((status == 429 || status == 503) && attempt < 4) {
            delay(1000L shl attempt)
            attempt++
            continue
        }
        val response = JSONObject(json)
        if (status >= 400) {
            throw MapricotException(status, "Mapricot returned $status: ${response.getJSONObject("error").getString("message")}")
        }
        return response
    }
}

The examples below run in a coroutine, such as lifecycleScope.launch { } in an activity.

Geocode an address

/v1/search turns an address or place name into coordinates:

val results = mapricot("/v1/search", mapOf("q" to "128 SW 3rd Ave, Portland", "limit" to 1)).getJSONArray("results")
for (i in 0 until results.length()) {
    val result = results.getJSONObject(i)
    println("${result.getString("label")} ${result.getDouble("lat")} ${result.getDouble("lon")}")
}
128 SW 3rd Avenue, Portland, OR, USA 45.521736 -122.673276

Each result also has its address in parts under address, and a type, such as address, street, or locality, that says how exact the match is. To limit results to some countries, add "country" to "US,CA". Geocoding lists every parameter and field.

Find the address at a point

/v1/reverse turns coordinates into the nearest address:

val nearest = mapricot("/v1/reverse", mapOf("lat" to 45.5217, "lon" to -122.6733, "limit" to 1))
    .getJSONArray("results").getJSONObject(0)
println("${nearest.getString("label")} ${nearest.getInt("distanceMeters")}")
128 SW 3rd Avenue, Portland, OR, USA 4

Suggest places as someone types

/v1/autocomplete takes the text typed so far. focus ranks results near a point first, such as the user's location:

val suggestion = mapricot("/v1/autocomplete", mapOf("q" to "stumpt", "focus" to "45.52,-122.68", "limit" to 1))
    .getJSONArray("results").getJSONObject(0)
println("${suggestion.getString("label")} ${suggestion.getString("id")}")
Stumptown Coffee Roasters, Portland, OR, USA mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzExOTg2MjI2NjYw

Find places nearby

/v1/nearby lists places of a category around a point, nearest first:

val nearby = mapricot(
    "/v1/nearby",
    mapOf("lat" to 45.52, "lon" to -122.68, "category" to "food:coffee_shop", "radius" to 1000, "limit" to 1),
).getJSONArray("results")
for (i in 0 until nearby.length()) {
    val place = nearby.getJSONObject(i)
    println("${place.getString("name")} ${place.getInt("distanceMeters")}")
}
Public Domain 37

Look up a place by ID

Every result has an id. Save it, and /v1/places/{id} fetches that place again later:

val saved = mapricot("/v1/places/${suggestion.getString("id")}").getJSONObject("result")
println(saved.getString("label"))
Stumptown Coffee Roasters, Portland, OR, USA

Get a route

/v1/route takes a JSON body with two to 20 waypoints and a mode of drive, walk, or bicycle:

val waypoints = JSONArray()
    .put(JSONObject().put("lat", 43.7396).put("lon", 7.4275))
    .put(JSONObject().put("lat", 43.734).put("lon", 7.4222))
val route = mapricot("/v1/route", body = JSONObject().put("waypoints", waypoints).put("mode", "walk"))
    .getJSONArray("routes").getJSONObject(0)
println("${route.getInt("distanceMeters")} ${route.getInt("durationSeconds")}")
1044 739

Distances are in meters and times in seconds. When no path connects the waypoints, routes is empty. The route's line is in polyline, encoded with six decimal places, so decode it with precision 6, as decoding a route polyline explains.

Look up an IP address

/v1/ip/{ip} gives the approximate location of an IPv4 or IPv6 address:

val location = mapricot("/v1/ip/8.8.8.8")
println("${location.optString("city")} ${location.optString("countryCode")}")
Mountain View US

Show a map

Map tiles are free and need no key. MapLibre Native for Android draws them. Add it to your module's dependencies:

implementation("org.maplibre.gl:android-sdk:13.7.1")

Follow MapLibre's getting started to add a MapView to your layout and pass it the activity's lifecycle calls. Then load a Mapricot style:

mapView.getMapAsync { map ->
    map.setStyle("https://tiles.mapricot.com/styles/light.json")
    map.cameraPosition = CameraPosition.Builder().target(LatLng(45.52, -122.68)).zoom(12.0).build()
}

The styles are light, dark, and grayscale, and they credit OpenStreetMap in the map's corner, which its license requires you to keep.

Errors and retries

mapricot throws a MapricotException for any error response, with the status and the API's message:

try {
    mapricot("/v1/search", mapOf("q" to ""))
} catch (error: MapricotException) {
    println(error.message)
}
Mapricot returned 400: The service rejected the request.

It retries 429 and 503 responses up to three times, waiting 2, 4, then 8 seconds. Errors aren't billed, so retrying them costs nothing. A 429 that keeps coming back can also mean the key's caps or the account's credit are used up. In a search box, wait until the user pauses typing before sending a request, so each keystroke isn't one. The API reference lists every status.