Navvo
Guides

Java SDK

Install and use the official io.navvo:navvo-java client for server-side geocoding, routing, places, traffic, EV, transit, and more.

Java SDK

The official io.navvo:navvo-java library is a server-side Java client for the Navvo Web Service. It is fully typed, retry-aware, thread-safe, and built on pure JDK 11 (java.net.http.HttpClient) with a small vendored JSON parser — zero third-party dependencies (no Jackson, no Guava). It is Kotlin-friendly and offers both synchronous methods and CompletableFuture async mirrors. There is no map UI or tile rendering — to render a map in the browser use Navvo Maps.

Requires: Java 11+ (works on 11 / 17 / 21) · Base URL: https://navvo.io/api/v1

Install

<dependency>
  <groupId>io.navvo</groupId>
  <artifactId>navvo-java</artifactId>
  <version>0.3.0</version>
</dependency>
import io.navvo.NavvoClient;
import io.navvo.model.*;

Authenticate

Every call is sent with the x-api-key header. Pass the key explicitly, or use NavvoClient.fromEnv() to read it from the NAVVO_API_KEY environment variable. The key shape (nvvo_<48 hex>) is validated up front, and the key is never logged — it is redacted to nvvo_****<last4> everywhere.

NavvoClient navvo = NavvoClient.fromEnv();                       // uses $NAVVO_API_KEY
NavvoClient navvo = NavvoClient.builder()
    .apiKey("nvvo_your_key_here")
    .build();

The SDK never puts the key in a URL — except styleUrl(), which builds a browser-fetchable map style URL where the key must ride as ?api_key= (browsers cannot set headers on tile/style requests).

Quickstart

import io.navvo.NavvoClient;
import io.navvo.model.*;
import java.util.*;

public class Demo {
    public static void main(String[] args) {
        try (NavvoClient navvo = NavvoClient.fromEnv()) {

            // Forward geocode — string coordinates are parsed to numbers for you.
            List<GeocodeResult> hits = navvo.geocode(
                GeocodeQuery.builder().q("Abdali Hospital, Amman").limit(5).build());
            GeocodeResult top = hits.get(0);
            System.out.println(top.lat() + "," + top.lng() + " " + top.displayName());

            // Plan a route — you pass LatLng(lat, lng); the SDK serialises lon/lat on the wire.
            RoutePlan plan = navvo.route(RoutePlanRequest.builder()
                .addLocation(LatLng.named(31.95, 35.91, "A"))
                .addLocation(LatLng.named(31.99, 35.95, "B"))
                .costing("auto")
                .build());
            System.out.println("ETA seconds: " + plan.durationSeconds());

            // Decode the leg polyline (polyline6) into points.
            List<LatLng> line = plan.geometry();
            System.out.println("shape points: " + line.size());

            // Live, condition-aware extras (NavvoObject with a raw() escape hatch).
            NavvoObject wx = navvo.weatherPoint(31.95, 35.91, "metric");
            System.out.println(wx.raw());
        }
    }
}

Configuration

Configure through the builder:

NavvoClient navvo = NavvoClient.builder()
    .apiKey(System.getenv("NAVVO_API_KEY"))
    .baseUrl("https://navvo.io/api/v1")
    .timeout(java.time.Duration.ofSeconds(15))
    .connectTimeout(java.time.Duration.ofSeconds(5))
    .userAgent("acme-dispatch/2.3")
    .retry(RetryConfig.builder()
        .retries(3)
        .baseDelay(java.time.Duration.ofMillis(500))
        .maxDelay(java.time.Duration.ofSeconds(20))
        .factor(2).jitter(true).respectRetryAfter(true)
        .build())
    .build();

Use navvo.withApiKey(key) to clone a client for a different key (multi-tenant servers). A NavvoClient is immutable and safe for concurrent use across threads. The client is AutoCloseable for try-with-resources.

Coordinates — never backwards

You always supply points as LatLng.of(lat, lng) (or LatLng.named(lat, lng, name) for a waypoint). The SDK owns every wire-format difference: {lon, lat} for route/trafficRoute, {lat, lon} for the routing passthrough payloads (simpleRoute/matrix/optimizeStops), separate lat/lon params for reverse geocode, west,south,east,north for BBox, [lng, lat] for GeoJSON, and WKT POINT(lng lat) for a place center (parse it with place.centerLatLng()).

LatLng amman = LatLng.of(31.9539, 35.9106);
BBox box     = BBox.fromCorners(35.85, 31.92, 35.98, 32.02); // west, south, east, north

Retries & errors

Transient failures (5xx, 408, 429/quota, network blips, and available:false envelopes) are retried with exponential backoff and full jitter, honouring Retry-After; client errors (400/401/403/404) are never retried. Every error is an unchecked subtype of NavvoException:

try {
    navvo.geocode(GeocodeQuery.of("Abdali Hospital, Amman"));
} catch (NavvoRateLimitException e) {
    log.warn("rate limited, retry after {}", e.retryAfter());
} catch (NavvoScopeException e) {
    log.error("scope/quota: {} (missing {})", e.reason(), e.scope());
} catch (NavvoServiceUnavailableException e) {
    log.error("geocoder upstream is down — fall back");
}

The geocoder/routing envelope ({ provider, available, data }) is unwrapped for you — an available:false body becomes a NavvoServiceUnavailableException — and an unknown place slug returns an empty Optional (a soft-null, not an error).

ExceptionFrom
NavvoConfigExceptionbad/missing key, bad option, non-HTTPS base URL
NavvoValidationException400 (and other non-retryable 4xx)
NavvoAuthException401
NavvoScopeException403 (has reason(), scope())
NavvoNotFoundException404
NavvoRateLimitException429 (has retryAfter(), limit(), remaining(), resetAt())
NavvoServiceUnavailableExceptionavailable:false, 502/503
NavvoServerExceptionother 5xx
NavvoTimeoutException / NavvoNetworkExceptiontimeout / transport

The API key is sent only in the x-api-key header and never appears in an error; errors carry redactedKey() (nvvo_****<last4>).

search() returns a typed SearchResponse for GET /search, including the queryId() that later click events refer to. An option you don't set is left out of the request:

SearchResponse res = navvo.search("pharmacy", SearchOptions.builder()
    .near(LatLng.of(31.9539, 35.9106))      // rank nearer places higher
    .radiusKm(3).sort(SearchSort.DISTANCE)  // both need near
    .openNow(true).minRating(4)
    .amenities("wheelchair-accessible-entrance")
    .facets(SearchFacetName.CATEGORY, SearchFacetName.AMENITIES)
    .highlight(true)
    .build());
res.queryId();                              // for sendSearchEvent()
for (Place p : res.places().items())        // may carry distanceKm(), imageUrl(), highlight(), promoted()
    System.out.println(p.nameEn() + " · " + p.distanceKm() + " km");
res.didYouMean(); res.suggestions();        // zero results only: a spelling fix, popular places

navvo.search("rumi caf", SearchOptions.builder().mode(SearchMode.TYPEAHEAD).build());          // as the user types
navvo.search("quiet place to work with good coffee",
    SearchOptions.builder().semantic(SearchSemantic.ON).build());                             // meaning-based matching
navvo.search("pharmacy", SearchOptions.builder().analytics(false).build());                   // keep this call out of analytics
navvo.searchSuggestions("pha", "en", 6);                                                       // GET /search/suggestions
navvo.similarPlaces(SimilarPlacesOptions.builder().slug("jordan-museum-994700").limit(6).build()); // GET /search/similar
  • SearchResponse is a NavvoObject, so code written for 0.1 (get(...), raw()) keeps compiling and working, including classes already compiled against it.
  • res.facets(), res.understanding() and res.index() are typed too; res.index().mode() is "hybrid" when meaning-based matching contributed.
  • With an API key, meaning-based matching runs only with semantic(SearchSemantic.ON), and never in typeahead mode.
  • searchSuggestions(null) returns a starter list of everyday categories and well-known places. similarPlaces() returns items() and source() ("vectors", compared by meaning, or the "nearby" fallback); an unknown place throws NavvoNotFoundException.
  • Before any request is sent, search() throws NavvoValidationException for a blank query and NavvoConfigException for an origin off the globe, a value outside the fixed option sets or an unusable session id.

Highlights arrive HTML-escaped, with matched words wrapped in <em>. highlight().nameEnSegments() and nameArSegments() split them into plain-text runs you can render in any format:

for (Place p : res.places().items())
    if (p.highlight() != null)
        for (HighlightSegment seg : p.highlight().nameEnSegments())
            System.out.print(seg.match() ? "[" + seg.text() + "]" : seg.text());

Price filter and result details

From 0.3.0, price(...) keeps places at any of the given price levels: 1 up to JOD 5 per person, 2 JOD 5–10, 3 JOD 10–20, 4 more. Every Place can also carry what a results list shows:

SearchResponse res = navvo.search("cafe", SearchOptions.builder()
    .near(LatLng.of(31.9539, 35.9106))
    .price(1, 2)
    .build());
for (Place p : res.places().items()) {
    System.out.println(p.nameEn() + " · " + p.categoryName() + " · " + p.priceRange() + " · " + p.reviewCount());
    PlaceHoursToday h = p.hours();          // today, in Jordan time; never null, isEmpty() without hours
    if (!h.isEmpty()) System.out.println(h.state() + " " + h.closesAt() + " " + h.opensAt() + " " + h.opensDay());
}
navvo.similarPlaces(SimilarPlacesOptions.builder()
    .slug("jordan-museum-994700").locale("ar").build());                  // categoryName() and address() in Arabic

priceLevel(), address(), area() and highlights() are there too. Each returns null, or an empty list, when the place has no data. h.isOpen() is true while a place is open or closing (isClosing(): closes within 45 minutes). alwaysOpen() marks a place open around the clock, and hoursMayVary() a public holiday. A level outside 1–4 throws NavvoConfigException before anything is sent. See Result details.

Clicks and conversions

To see click-through and conversions for your project in the developer portal, send a session id with the search, one per end-user session and only if your user agreed to it. Then report what the user opened:

String session = NavvoClient.createSearchSessionId(); // one per end-user session, only with the user's consent
SearchResponse res = navvo.search("rumi cafe", SearchOptions.builder().session(session).build());
Place hit = res.places().items().get(0);              // the result the user opened

navvo.sendSearchEvent(SearchEvent.click(res.queryId())
    .placeId(hit.id()).position(res.positionOf(0)).build());
navvo.sendSearchEvent(SearchEvent.conversion(res.queryId(), SearchEventName.DIRECTIONS)
    .placeId(hit.id()).position(res.positionOf(0)).build());
  • Events are accepted for 7 days after the search, only when it carried a session id, and only with the same API key.
  • sendSearchEvents() sends several at once, in requests of at most 20 events and 16 KB, and sums accepted(), rejected() and reasons(). Conversion names are DIRECTIONS, NAVIGATE, SAVE, CALL, WEBSITE and SHARE.
  • NavvoClient.isValidSearchSessionId(id) checks an id; search() throws NavvoConfigException for an invalid one, and createSearchSessionId() always makes a valid one.
  • NavvoClient.builder().searchAnalytics(false) keeps everything the client searches out of analytics. Events sent by such a client are rejected, so report clicks from a client built without it.

See Search events for what is accepted and why an event can be rejected, and Search analytics & privacy for consent and what Navvo records.

Method map

AreaMethods
Tiles & configstyles, styleUrl, mapConfig
Search & geocodingsearch, searchSuggestions, similarPlaces, sendSearchEvent, sendSearchEvents, geocode, reverseGeocode, geocodeBatch, reverseGeocodeBatch
Placesplaces, placesStream, place, placeCategories, placeFacets, placeThumb, placeHours, placePopularTimes, placeAttributes, placeReviewsSummary, placePhotos, placeNearby, placesReachable
Routingroute, simpleRoute, matrix, isochrone, mapMatch, optimizeStops, optimize, routingProfiles
Traffic & incidentstrafficRoute, trafficSummary, incidents, incidentTypes, reportIncident, roadEdits, routeRisk
Weather & air qualityweatherPoint, weatherForecast, weatherCities, airQualityCities
EV & transitevStations, transitStops, gbfs

Each method maps to the endpoint documented in the API reference. Typed results expose raw() with the full untyped body for forward compatibility; less-structured responses return a NavvoObject whose raw() returns the parsed tree. optimize requires the opt-in optimize scope; trafficRoute requires both routing and traffic. See Route Optimization and Traffic & Incidents.

Pagination & async

GET /places is paginated; placesStream(query) walks every page lazily:

navvo.placesStream(PlaceListQuery.builder().category("restaurants").limit(100).build())
     .forEach(p -> System.out.println(p.nameEn()));

Key methods have async mirrors returning CompletableFuture:

navvo.geocodeAsync(GeocodeQuery.of("Amman"));
navvo.routeAsync(req);
navvo.placeAsync("ardh-cafe-ard-kafyh-2bd24a");

Building without Maven

The library compiles with the JDK alone:

javac --release 11 -d build/classes $(find src/main/java -name '*.java')
jar --create --file navvo-java-0.3.0.jar -C build/classes .
java -cp navvo-java-0.3.0.jar io.navvo.examples.Main   # offline smoke test

A pom.xml is included for publishing to Maven Central (mvn clean package produces the binary, -sources, and -javadoc jars).

Publishing & versioning

The artifact is published to Maven Central. The current version is 0.3.0:

<dependency>
  <groupId>io.navvo</groupId>
  <artifactId>navvo-java</artifactId>
  <version>0.3.0</version>
</dependency>
Copyright © 2026