Go SDK
Go SDK
The official github.com/navvo/navvo-go module (package navvo) is a
server-side Go client for the Navvo Web Service. It is fully typed,
retry-aware, concurrency-safe, and uses only the standard library
(net/http, encoding/json, context) — no third-party dependencies.
Every call takes a context.Context first argument. There is no map UI or
tile rendering — for rendering a map in the browser use
Navvo Maps.
Requires: Go 1.21+ · Base URL: https://navvo.io/api/v1
Install
go get github.com/navvo/navvo-go
import navvo "github.com/navvo/navvo-go"
Authenticate
Every call is sent with the x-api-key header. Pass the key explicitly, or pass
"" to read it from the NAVVO_API_KEY environment variable. The key shape
(nvvo_<48 hex>) is validated up front.
c, err := navvo.New("") // uses $NAVVO_API_KEY
c, err := navvo.New("nvvo_your_key_here") // explicit
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
package main
import (
"context"
"fmt"
"log"
"time"
navvo "github.com/navvo/navvo-go"
)
func main() {
c, err := navvo.New("") // reads NAVVO_API_KEY
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Forward geocode — string coordinates are parsed to numbers for you.
hits, err := c.Geocode(ctx, "Abdali Hospital, Amman", &navvo.GeocodeOptions{Limit: 5})
if err != nil {
log.Fatal(err)
}
fmt.Println(hits[0].Lat, hits[0].Lng, hits[0].DisplayName)
// Plan a route — you pass {Lat, Lng}; the SDK serialises lon/lat on the wire.
plan, err := c.Route(ctx, navvo.RoutePlanRequest{
Locations: []navvo.LatLng{
{Lat: 31.95, Lng: 35.91, Name: "A"},
{Lat: 31.99, Lng: 35.95, Name: "B"},
},
Costing: "auto",
})
if err != nil {
log.Fatal(err)
}
fmt.Println("ETA seconds:", plan.Trip().Summary.Time)
// Decode the leg polyline (polyline6) into points.
line := plan.Geometry()
fmt.Println("shape points:", len(line))
// Live, condition-aware extras.
wx, _ := c.WeatherPoint(ctx, 31.95, 35.91, "metric")
fmt.Println(wx.Temperature, wx.Unit, "·", *wx.AQICategory)
}
Configuration
New accepts functional options:
c, err := navvo.New(
"",
navvo.WithBaseURL("https://navvo.io/api/v1"),
navvo.WithTimeout(15*time.Second),
navvo.WithUserAgent("acme-dispatch/2.3"),
navvo.WithRetry(navvo.RetryConfig{
Retries: 3, BaseDelay: 500 * time.Millisecond, Factor: 2, Jitter: true, MaxDelay: 20 * time.Second,
}),
)
Use navvo.WithHTTPClient(myClient) to bring your own pooled *http.Client, and
c.WithAPIKey(key) to clone a client for a different key (multi-tenant servers).
A *Client is safe for concurrent use across goroutines.
Coordinates — never backwards
You always supply points as navvo.LatLng{Lat, Lng}. The SDK owns every
wire-format difference: {lon, lat} for Route/TrafficRoute, {lat, lon} for
the routing passthrough payloads (SimpleRoute/Matrix/Optimize), separate
lat/lon params for reverse geocode, west,south,east,north for BBox,
[lng, lat] for GeoJSON, and WKT POINT(lng lat) for place center (parse it
with navvo.ParseWKTPoint(place.Center) or place.CenterLatLng()).
amman := navvo.LatLng{Lat: 31.9539, Lng: 35.9106}
box := navvo.BBox{West: 35.85, South: 31.92, East: 35.98, North: 32.02}
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 wraps *navvo.APIError and supports errors.Is /
errors.As:
hits, err := c.Geocode(ctx, "Abdali Hospital, Amman", nil)
if err != nil {
var rl *navvo.RateLimitError
var scope *navvo.ScopeError
switch {
case errors.As(err, &rl):
log.Printf("rate limited, retry after %v", rl.RetryAfter)
case errors.As(err, &scope):
log.Printf("scope/quota problem: %s (missing %s)", scope.Reason, scope.Scope)
case errors.Is(err, navvo.ErrServiceUnavailable):
log.Print("geocoder upstream is down — fall back")
default:
return err
}
}
The geocoder/routing envelope ({ provider, available, data }) is unwrapped
for you — an available:false body becomes a *navvo.ServiceUnavailableError —
and an unknown place slug returns nil (a soft-null, not an error). Cancel or
deadline any call through the context.Context you pass.
| Error | Sentinel | From |
|---|---|---|
*ConfigError | ErrConfig | bad/missing key, bad option |
*ValidationError | ErrValidation | 400 (has Fields) |
*AuthError | ErrUnauthorized | 401 |
*ScopeError | ErrForbidden | 403 (has Reason, Scope) |
*NotFoundError | ErrNotFound | 404 |
*RateLimitError | ErrRateLimited | 429 (has RetryAfter) |
*ServiceUnavailableError | ErrServiceUnavailable | available:false, 502/503 |
*ServerError | ErrServer | other 5xx |
*TimeoutError / *NetworkError | ErrTimeout / ErrNetwork | 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 the typed GET /search result, including the QueryID that later click events refer to. Options left at their zero value are not sent:
res, err := c.Search(ctx, "pharmacy", &navvo.SearchOptions{
Near: &navvo.LatLng{Lat: 31.9539, Lng: 35.9106}, // rank nearer places higher
RadiusKm: 3, // both RadiusKm and SortDistance need Near
Sort: navvo.SearchSortDistance,
OpenNow: navvo.Bool(true),
MinRating: 4,
Amenities: []string{"wheelchair-accessible-entrance"},
Facets: []navvo.SearchFacet{navvo.SearchFacetCategory, navvo.SearchFacetAmenities},
Highlight: navvo.Bool(true),
})
if err != nil {
return err
}
fmt.Println(res.QueryID) // for SendSearchEvent
for i, p := range res.Places.Items {
// DistanceKm and Highlight are nil when they do not apply; ImageURL is "" without a picture.
fmt.Println(res.Places.Position(i), p.NameEn, p.DistanceKm, p.ImageURL, p.Promoted)
}
fmt.Println(res.DidYouMean, len(res.Suggestions)) // zero results only: a spelling fix, popular places
c.Search(ctx, "rumi caf", &navvo.SearchOptions{Mode: navvo.SearchModeTypeahead}) // as the user types
c.Search(ctx, "quiet place to work with good coffee", &navvo.SearchOptions{Semantic: navvo.SearchSemanticOn}) // meaning-based matching
c.Search(ctx, "pharmacy", &navvo.SearchOptions{Analytics: navvo.Bool(false)}) // keep this call out of analytics
c.SearchSuggestions(ctx, "pha", &navvo.SearchSuggestionsOptions{Limit: 6}) // GET /search/suggestions
c.SimilarPlaces(ctx, navvo.SimilarPlacesOptions{Slug: "jordan-museum-994700", Limit: 6}) // GET /search/similar
res.QueryUnderstanding()says how the query was read (language, intent, category, locality, open now), andres.IndexStatus().Modeis"hybrid"when meaning-based matching contributed.res.Rawholds the complete body.- With an API key, meaning-based matching runs only with
Semantic: navvo.SearchSemanticOn, and never in typeahead mode. SearchSuggestionswith an empty query returns a starter list of everyday categories and well-known places.SimilarPlacestakes anIDor aSlugand returnsItemsandSource(SimilarSourceVectors, compared by meaning, or theSimilarSourceNearbyfallback); an unknown place is a*NotFoundError.Searchrejects a blank query, an origin off the globe and a malformed session id with a*ValidationErrorbefore anything is sent.
Highlights arrive HTML-escaped, with matched words wrapped in <em>. ParseHighlight splits one into plain-text segments, so you can render them as text instead of trusting the string as markup:
for _, p := range res.Places.Items {
if p.Highlight != nil { // nil when no name matched, or highlights were not requested
for _, seg := range navvo.ParseHighlight(p.Highlight.NameEn) {
fmt.Print(seg.Text, seg.Match)
}
}
}
Price filter and result details
From v0.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 SearchItem can also carry what a results list shows. A zero value (0, "", nil) means the place has no data:
res, err := c.Search(ctx, "cafe", &navvo.SearchOptions{
Near: &navvo.LatLng{Lat: 31.9539, Lng: 35.9106},
Price: []int{1, 2},
})
if err != nil {
return err
}
for _, p := range res.Places.Items {
fmt.Println(p.NameEn, p.CategoryName, p.PriceRange, p.PriceLevel, p.ReviewCount, p.Address, p.Area, p.Highlights)
if p.Hours != nil { // today, in Jordan time
fmt.Println(p.Hours.State, p.Hours.ClosesAt, p.Hours.OpensAt, p.Hours.OpensDay)
}
}
c.SimilarPlaces(ctx, navvo.SimilarPlacesOptions{Slug: "jordan-museum-994700", Locale: "ar"}) // CategoryName and Address in Arabic
Hours.State is navvo.SearchHoursOpen, navvo.SearchHoursClosing (closes within 45 minutes) or navvo.SearchHoursClosed. Hours.AlwaysOpen marks a place open around the clock, and Hours.HoursMayVary a public holiday. A level outside 1–4 returns a *ValidationError 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:
session := navvo.NewSearchSessionID() // one per end-user session, only with the user's consent
res, err := c.Search(ctx, "rumi cafe", &navvo.SearchOptions{Session: session})
if err != nil || len(res.Places.Items) == 0 {
return err
}
hit, position := res.Places.Items[0], res.Places.Position(0) // the result the user opened
c.SendSearchEvent(ctx, navvo.SearchEvent{
QueryID: res.QueryID, Type: navvo.SearchEventClick, PlaceID: hit.ID, Position: position,
})
c.SendSearchEvent(ctx, navvo.SearchEvent{
QueryID: res.QueryID, Type: navvo.SearchEventConversion, Name: navvo.SearchEventDirections,
PlaceID: hit.ID, Position: position,
})
- Events are accepted for 7 days after the search, only when it carried a session id, and only with the same API key.
SendSearchEventssends several at once, in requests of at most 20 events and 16 KB, and adds upAccepted,RejectedandReasons. Conversion names areSearchEventDirections,SearchEventNavigate,SearchEventSave,SearchEventCall,SearchEventWebsiteandSearchEventShare.NewSearchSessionID()always makes a valid id, andValidSearchSessionID(id)checks one: 16–64 characters ofA-Z a-z 0-9 _ -with no run of 6 digits.navvo.WithSearchAnalytics(false)keeps everything a client searches out of analytics. Events sent by such a client are rejected, so report clicks from a client created 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 |
| Places | ListPlaces, Place, PlaceThumb, PlaceCategories |
| Routing | Route, SimpleRoute, Matrix, 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. Results expose a Raw field
(map[string]any) with the full untyped body for forward compatibility.
Optimize requires the opt-in optimize scope; TrafficRoute requires both
routing and traffic. See Route Optimization
and Traffic & Incidents.
Publishing & versioning
The module is published via Git tags. The current version is v0.3.0:
go get github.com/navvo/[email protected]