Navvo
Guides

Go SDK

Install and use the official github.com/navvo/navvo-go client for server-side geocoding, routing, places, traffic, EV, transit, and more.

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.

ErrorSentinelFrom
*ConfigErrorErrConfigbad/missing key, bad option
*ValidationErrorErrValidation400 (has Fields)
*AuthErrorErrUnauthorized401
*ScopeErrorErrForbidden403 (has Reason, Scope)
*NotFoundErrorErrNotFound404
*RateLimitErrorErrRateLimited429 (has RetryAfter)
*ServiceUnavailableErrorErrServiceUnavailableavailable:false, 502/503
*ServerErrorErrServerother 5xx
*TimeoutError / *NetworkErrorErrTimeout / ErrNetworktimeout / 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 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), and res.IndexStatus().Mode is "hybrid" when meaning-based matching contributed. res.Raw holds the complete body.
  • With an API key, meaning-based matching runs only with Semantic: navvo.SearchSemanticOn, and never in typeahead mode.
  • SearchSuggestions with an empty query returns a starter list of everyday categories and well-known places. SimilarPlaces takes an ID or a Slug and returns Items and Source (SimilarSourceVectors, compared by meaning, or the SimilarSourceNearby fallback); an unknown place is a *NotFoundError.
  • Search rejects a blank query, an origin off the globe and a malformed session id with a *ValidationError before 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.
  • SendSearchEvents sends several at once, in requests of at most 20 events and 16 KB, and adds up Accepted, Rejected and Reasons. Conversion names are SearchEventDirections, SearchEventNavigate, SearchEventSave, SearchEventCall, SearchEventWebsite and SearchEventShare.
  • NewSearchSessionID() always makes a valid id, and ValidSearchSessionID(id) checks one: 16–64 characters of A-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

AreaMethods
Tiles & configStyles, StyleURL, MapConfig
Search & geocodingSearch, SearchSuggestions, SimilarPlaces, SendSearchEvent, SendSearchEvents, Geocode, ReverseGeocode, GeocodeBatch
PlacesListPlaces, Place, PlaceThumb, PlaceCategories
RoutingRoute, SimpleRoute, Matrix, 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. 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]
Copyright © 2026