Vietnam Directions API: Motorbike Routing & Turn-by-Turn the Google-Compatible Way
A developer how-to for the GoGoDuk Directions API: call /v1/directions, read real ETA and distance, decode the polyline onto your map, render turn-by-turn, and handle the ESTIMATED fallback.
Most location features in Vietnamese products start with a distance circle: "we deliver within 5 km." It is easy to ship and almost always wrong. In real cities, two points that are 3 km apart as the crow flies can be a 6 km ride once you account for rivers, one-way streets, bridges, and alleys—and a motorbike takes a very different path than a car. If your delivery fee, ETA, or driver assignment is built on straight-line distance, it drifts away from reality the moment traffic gets dense.
This how-to walks through integrating the GoGoDuk Directions API end to end: calling GET /v1/directions, reading the real distance and ETA, decoding the route polyline onto a map, rendering turn-by-turn instructions, and handling the straight-line fallback gracefully. The response is intentionally Google-Directions-compatible, so if you already speak that shape, most of your existing code carries over.
Why real routing beats straight-line distance
A routing engine answers a different question than a distance formula. Instead of "how far apart are these two points," it answers "what path does a vehicle actually take, and how long does it take." For Vietnam, the vehicle detail is the whole game:
- Motorbikes and cars route differently. Bikes cut through narrow lanes and alleys (
hẻm/ngõ) that cars cannot enter, and they are exempt from some car-only restrictions. A car-profile route over-estimates both distance and time for the way most Vietnamese deliveries actually move. - Straight-line distance under-charges and over-promises. A radius-based fee is cheap in the easy cases and badly wrong across a river or a one-way grid. A radius-based ETA promises arrivals your driver cannot hit.
- The road graph changes. Real road networks have turn restrictions, dead ends, and disconnected segments. Only a routing engine that walks the graph captures them.
GoGoDuk's routing runs on the OpenStreetMap road graph tuned for a motobike profile, so the path—and the estimated time—match how a real rider would go. If you are new to the platform, the GoGoDuk developer overview covers the rest of the API surface (geocoding, suggest, boundaries).
The directions endpoint at a glance
The endpoint is a single GET request. You pass an origin and a destination as lat,lng pairs (the same order Google and Goong use) and a vehicle profile.
| Parameter | Required | Format | Notes |
| --- | --- | --- | --- |
| origin | Yes | lat,lng | Start point, WGS84 (latitude first) |
| destination | Yes | lat,lng | End point, WGS84 |
| vehicle | No | enum | motobike (default). car is planned. Other values return 400 INVALID_VEHICLE. |
Authentication uses the X-API-Key header (keep the key server-side), and the key needs the routing:read scope. A minimal call looks like this:
curl -G "https://api.gogoduk.com/v1/directions" \
-H "X-API-Key: $GOGODUK_API_KEY" \
--data-urlencode "origin=21.0285,105.8542" \
--data-urlencode "destination=21.2212,105.8072" \
--data-urlencode "vehicle=motobike"A Google-compatible response
The response mirrors the Google Directions API structure, so a team migrating from Google or Goong rarely has to reshape anything. The shape is routes[] → legs[] → steps[], with a status flag at the top:
| Field | Meaning |
| --- | --- |
| status | OK for a real road route, ESTIMATED for a straight-line fallback |
| routes[].legs[].distance | { text: "5.7 km", value: 5704.1 } — value is in meters |
| routes[].legs[].duration | { text: "9 phút", value: 511.2 } — value is in seconds |
| routes[].legs[].steps[] | Turn-by-turn steps with html_instructions, maneuver, and a per-step polyline |
| routes[].overview_polyline.points | Encoded polyline for the whole route (precision 5) |
| routes[].bounds | { northeast, southwest } for fitting the map viewport |
A trimmed response:
{
"status": "OK",
"routes": [
{
"legs": [
{
"distance": { "text": "5.7 km", "value": 5704.1 },
"duration": { "text": "9 phút", "value": 511.2 },
"steps": [
{
"distance": { "text": "243 m", "value": 243.4 },
"html_instructions": "Head northwest on Pasteur",
"maneuver": "turn-right",
"polyline": { "points": "suw`Aq{fjSOF{Bx@..." },
"travel_mode": "DRIVING"
}
]
}
],
"overview_polyline": { "points": "suw`Aq{fjS..." }
}
]
}If you already parse Google Directions, the field names line up. The two things to watch are units (distance.value in meters, duration.value in seconds) and the status flag, which is where the fallback behavior lives.
Calling the API and reading ETA and distance
Call the endpoint from your server, then read the leg-level distance and duration. Those two numbers are usually all a delivery or ride-hailing flow needs for pricing and an arrival estimate.
// Server-side: fetch a route and read real distance + ETA
type LatLng = { lat: number; lng: number };
async function getRoute(origin: LatLng, destination: LatLng) {
const params = new URLSearchParams({
origin: `${origin.lat},${origin.lng}`,
destination: `${destination.lat},${destination.lng}`,
vehicle: "motobike",
});
const res = await fetch(`https://api.gogoduk.com/v1/directions?${params}`, {
headers: { "X-API-Key": process.env.GOGODUK_API_KEY! },
});
if (!res.ok) throw new Error(`Directions failed: ${res.status}`);
const data = await res.json();
const leg = data.routes?.[0]?.legs?.[0];
return {
status: data.status as "OK" | "ESTIMATED",
distanceMeters: leg?.distance?.value ?? 0,
durationSeconds: leg?.duration?.value ?? 0,
distanceText: leg?.distance?.text ?? "",
durationText: leg?.duration?.text ?? "",
overview: data.routes?.[0]?.overview_polyline?.points as string | undefined,
};
}Because the shape matches Google, a migration is mostly swapping the URL and the auth header. The Google version reads data.routes[0].legs[0].distance.value exactly the same way—you point the same parser at https://api.gogoduk.com/v1/directions with an X-API-Key header instead of a key= query parameter, which is the same swap described in migrating from Google Maps to a Vietnam map API.
With distanceMeters and durationSeconds in hand, pricing is a pure function: a base fee plus a per-kilometer rate, and an ETA you can show the customer in durationText.
Drawing the route: decode the polyline
overview_polyline.points is an encoded polyline at precision 5—the exact format Google uses—so any standard decoder works. Decode it to an array of [lat, lng] and draw it as a line on MapLibre, Leaflet, or Mapbox.
import polyline from "@mapbox/polyline"; // or @googlemaps/polyline-codec
// `overview` is routes[0].overview_polyline.points
function toGeoJsonLine(overview: string) {
const coords = polyline
.decode(overview, 5) // precision 5
.map(([lat, lng]) => [lng, lat]); // GeoJSON is [lng, lat]
return {
type: "Feature" as const,
geometry: { type: "LineString" as const, coordinates: coords },
properties: {},
};
}
// MapLibre GL example
map.addSource("route", { type: "geojson", data: toGeoJsonLine(overview) });
map.addLayer({
id: "route",
type: "line",
source: "route",
paint: { "line-color": "#2563eb", "line-width": 5 },
});The only gotcha is precision: decode with 5, and remember decoders return [lat, lng] while GeoJSON wants [lng, lat]. Use the route's bounds to fit the viewport so the whole line is visible on first paint.
Turn-by-turn from the steps array
For navigation UIs, walk legs[].steps[]. Each step carries a human-readable html_instructions, an optional Google-style maneuver code (turn-left, turn-right, roundabout-right, …), its own distance, and a per-step polyline you can highlight as the rider progresses.
type Step = {
html_instructions: string;
maneuver?: string;
distance: { text: string; value: number };
polyline: { points: string };
};
function toDirectionsList(legs: { steps: Step[] }[]) {
return legs.flatMap((leg) =>
leg.steps.map((step) => ({
instruction: step.html_instructions, // e.g. "Head northwest on Pasteur"
maneuver: step.maneuver ?? "straight", // map to your own icon set
distanceText: step.distance.text, // "243 m"
})),
);
}Map each maneuver value to an icon in your design system, and render the list under the map. Because the codes match Google's, an existing icon mapping built for Google Directions can be reused as-is.
Always have a line: handling ESTIMATED
When there is no real road route between the two points—an island, a disconnected piece of the network, or a bad coordinate—the API does not error out. Instead it returns a straight-line estimate so your UI always has something to draw:
statusbecomes"ESTIMATED".distanceis the great-circle distance multiplied by a road-detour factor;durationis estimated from an average speed.- The leg has a single step from origin to destination.
Branch on status so an estimate is never mistaken for a real route:
const route = await getRoute(origin, destination);
if (route.status === "ESTIMATED") {
// Draw a dashed line and label it as an estimate
map.setPaintProperty("route", "line-dasharray", [2, 2]);
showBadge("Estimated — no road route found");
} else {
map.setPaintProperty("route", "line-dasharray", [1]); // solid
}A simple rule of thumb: render ESTIMATED routes with a dashed line and a label, and consider blocking checkout (or flagging for review) when a delivery quote relies on an estimate rather than a real route.
Putting it together: a delivery ETA flow
Here is how the pieces line up for a typical "quote a delivery" flow:
- Capture both addresses. Use
GET /v1/suggestfor autocomplete and resolve the chosen result to coordinates. The UX details—debounce, keyboard navigation, conversion—are covered in Vietnam address autocomplete for checkout UX. - Request the route. Call
getRoute(pickup, dropoff)withvehicle=motobike. - Price and promise. Compute the fee from
distanceMeters(base + per-km) and show the ETA fromdurationText. - Draw it. Decode
overview_polylineonto the map and render turn-by-turn fromsteps. - Guard the edges. If
status === "ESTIMATED", mark the quote as approximate.
If your pricing also depends on which district or ward the drop-off falls in, pair this with reverse geocoding and boundary lookups—see working with Vietnam administrative boundaries for zone-based logic on top of the route.
In short: real motorbike routing replaces a guess with a number you can charge against and an ETA you can keep. The response is Google-compatible, so most integrations are a URL-and-header swap plus a status check for the fallback. Create a key with the routing:read scope in the dashboard (the free tier is 100 requests/day, no card required), explore the endpoints in the GoGoDuk docs, and try a live route on the interactive Vietnam map.
Facing performance issues or scaling challenges?
I specialize in building low-latency map infrastructure, real-time streaming pipelines (Kafka, ClickHouse), and highly optimized backend systems. Let's work together to scale your product.
Related Articles
12 Jun 2026
Migrating from Google Maps API to a Vietnam Map API: Cost & Code
Compare Google Maps Platform pricing and Vietnam limitations, then migrate geocoding, reverse geocoding, and address autocomplete to GoGoDuk with real code.
8 Jun 2026
Redis 8.8 Released: New Native Rate Limiter, Array Data Structure, and Up to +83% Performance Boost
A deep technical breakdown of the newly released Redis 8.8 (June 2, 2026). Explore the new O(1) sparse-friendly Array structure by antirez, the native INCREX rate-limiting command, and Hash field-level subkey notifications.
8 Jun 2026
PostGIS Performance Tuning: From 2s to 10ms for Vietnamese Spatial Queries (Gogoduk Case Study)
Explore practical PostGIS database optimization techniques from the Gogoduk Map API project. Learn how migrating from Geometry to Geography, designing Partial GIST indexes, and simplifying polygons can achieve 10ms query times.
8 Jun 2026
Redis Lua Script & SETNX: High-Performance Rate Limiting & Quota Alerting for APIs
Learn how Gogoduk builds API Rate Limiting and Quota Alerting systems with Redis and Go. Discover how to use Lua scripts for atomicity, prevent memory leaks, and leverage SETNX to deduplicate notifications.