Java SDK
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>
implementation 'io.navvo:navvo-java:0.3.0'
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).
| Exception | From |
|---|---|
NavvoConfigException | bad/missing key, bad option, non-HTTPS base URL |
NavvoValidationException | 400 (and other non-retryable 4xx) |
NavvoAuthException | 401 |
NavvoScopeException | 403 (has reason(), scope()) |
NavvoNotFoundException | 404 |
NavvoRateLimitException | 429 (has retryAfter(), limit(), remaining(), resetAt()) |
NavvoServiceUnavailableException | available:false, 502/503 |
NavvoServerException | other 5xx |
NavvoTimeoutException / NavvoNetworkException | timeout / 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
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
SearchResponseis aNavvoObject, so code written for 0.1 (get(...),raw()) keeps compiling and working, including classes already compiled against it.res.facets(),res.understanding()andres.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()returnsitems()andsource()("vectors", compared by meaning, or the"nearby"fallback); an unknown place throwsNavvoNotFoundException.- Before any request is sent,
search()throwsNavvoValidationExceptionfor a blank query andNavvoConfigExceptionfor 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 sumsaccepted(),rejected()andreasons(). Conversion names areDIRECTIONS,NAVIGATE,SAVE,CALL,WEBSITEandSHARE.NavvoClient.isValidSearchSessionId(id)checks an id;search()throwsNavvoConfigExceptionfor an invalid one, andcreateSearchSessionId()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
| Area | Methods |
|---|---|
| Tiles & config | styles, styleUrl, mapConfig |
| Search & geocoding | search, searchSuggestions, similarPlaces, sendSearchEvent, sendSearchEvents, geocode, reverseGeocode, geocodeBatch, reverseGeocodeBatch |
| Places | places, placesStream, place, placeCategories, placeFacets, placeThumb, placeHours, placePopularTimes, placeAttributes, placeReviewsSummary, placePhotos, placeNearby, placesReachable |
| Routing | route, simpleRoute, matrix, isochrone, mapMatch, optimizeStops, optimize, routingProfiles |
| Traffic & incidents | trafficRoute, trafficSummary, incidents, incidentTypes, reportIncident, roadEdits, routeRisk |
| Weather & air quality | weatherPoint, weatherForecast, weatherCities, airQualityCities |
| EV & transit | evStations, 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>