All posts

Calculate Shipping Fees by Distance with a Directions API

Compute real shipping fees from actual route distance and time using GoGoDuk's /v1/directions — motorbike-optimized routing for Vietnam, free forever.

A flat shipping fee sounds simple, but it drifts off quickly: far orders lose money, while nearby customers see an inflated price and abandon the cart. A fairer model charges by the actual distance and time of the route. This guide pulls those two numbers from GoGoDuk's Directions API and turns them into a shipping-fee formula — with motorbike-optimized routing, the way most delivery riders in Vietnam actually travel.

Why real road distance beats straight-line

Many apps compute fees from the straight-line (Haversine) distance between two coordinates. The problem: straight-line distance is almost always shorter than the real route. A shop in District 1 delivering to Thu Duc (HCMC) might be only ~9 km as the crow flies, but the real motorbike route — bridges, one-way streets, roundabouts — is meaningfully longer. Charging by straight-line distance means subsidizing every long-haul order.

This is a different model from zone-based pricing, where you split the city into districts/wards and assign each a fixed fee. If that fits you better, see Build delivery zones. A quick rule of thumb:

  • Zone-based — simple and predictable; good when you serve a handful of inner districts.
  • Distance-based — precise and fair; good when your delivery radius is wide or you want fees to reflect each order's true cost.

Get distance & duration from /v1/directions

The endpoint returns a Google Directions–compatible shape, so the two fields we need sit right in routes[0].legs[0].

Request

Call it directly with curl or fetch — the TypeScript SDK doesn't wrap directions yet, so use a raw request. Example: a District 1 shop delivering to a customer in Thu Duc:

curl -G "https://api.gogoduk.com/v1/directions" \
  -H "X-API-Key: $GOGODUK_API_KEY" \
  --data-urlencode "origin=10.7769,106.7009" \
  --data-urlencode "destination=10.8505,106.7717" \
  --data-urlencode "vehicle=motobike"
const res = await fetch(
  "https://api.gogoduk.com/v1/directions?" +
    new URLSearchParams({
      origin: "10.7769,106.7009",       // shop in District 1
      destination: "10.8505,106.7717",  // customer in Thu Duc
      vehicle: "motobike",
    }),
  { headers: { "X-API-Key": process.env.GOGODUK_API_KEY! } },
);
const { status, routes } = await res.json();
  • origin / destinationlat,lng format.
  • vehicle — currently motobike (default); car is coming later.
  • Required key scope: routing:read.

Response

Trimmed JSON — we only care about distance.value and duration.value of the first leg:

{
  "status": "OK",
  "routes": [
    {
      "legs": [
        {
          "distance": { "text": "12.4 km", "value": 12380.5 },
          "duration": { "text": "28 min", "value": 1683.0 }
        }
      ]
    }
  ]
}

Watch the units — this is the easiest place to introduce a bug:

  • distance.value is in meters (divide by 1000 for km).
  • duration.value is in seconds (divide by 60 for minutes).

The shipping-fee formula

Keep the fee logic in a pure function — easy to test and reuse. The VND numbers below are illustrative only — not GoGoDuk's pricing, and not any courier's tariff; swap in your own policy.

type FeeConfig = {
  baseFee: number;      // fee for the first leg (e.g. first 2 km)
  baseKm: number;       // km already included in baseFee
  perKm: number;        // fee per km beyond baseKm
  roundTo: number;      // round the fee up to this multiple
};
 
// ILLUSTRATIVE ONLY — replace with your own pricing policy.
const config: FeeConfig = {
  baseFee: 15000,   // 15,000 VND for the first 2 km
  baseKm: 2,
  perKm: 5000,      // 5,000 VND per additional km
  roundTo: 1000,    // round up to 1,000 VND
};
 
function calcShippingFee(distanceMeters: number, cfg: FeeConfig): number {
  const km = distanceMeters / 1000;
  const extraKm = Math.max(0, Math.ceil(km - cfg.baseKm)); // round up extra km
  const raw = cfg.baseFee + extraKm * cfg.perKm;
  return Math.ceil(raw / cfg.roundTo) * cfg.roundTo;
}
 
// Example: 12,380.5 m -> 12,380.5 / 1000 = 12.38 km
// extraKm = ceil(12.38 - 2) = 11  ->  15,000 + 11 * 5,000 = 70,000 VND
calcShippingFee(12380.5, config); // 70000

A time-based surcharge

duration.value lets you add a surcharge for routes that take longer (peak hours, traffic) — distance and duration complement each other:

function withTimeSurcharge(
  baseFee: number,
  durationSeconds: number,
): number {
  const minutes = durationSeconds / 60;
  // ILLUSTRATIVE: beyond 30 min, add 1,000 VND per extra 10 min.
  if (minutes <= 30) return baseFee;
  const extraBlocks = Math.ceil((minutes - 30) / 10);
  return baseFee + extraBlocks * 1000;
}

Handling ESTIMATED routes

When the engine can't connect two points (islands, broken road network, off coordinates), GoGoDuk doesn't return an error — it returns an estimated route with status: "ESTIMATED": distance becomes the straight-line distance times a road factor, and duration is estimated from an average speed. You always get a number to bill against, but handle it transparently:

if (status === "ESTIMATED") {
  // either flag the price as "estimated" to the customer,
  // or require manual confirmation before placing the order.
}

Two common approaches: (1) still quote a fee but mark it "estimated", or (2) for high-value orders, require staff confirmation before checkout.

Production notes

  • Compute the fee on the backend, not the client. If browser JavaScript computes it, a customer can tamper with the price before submitting the order.
  • Cache by origin→destination pair. The same shop delivering to the same address has a fixed distance — cache it instead of calling on every request.
  • Point-to-point only for now — there's no distance-matrix endpoint. For an order with multiple drops, call /v1/directions once per leg (batch matrix is on the roadmap, not yet available).
  • When a customer drops a pin instead of typing an address, use reverse geocoding to get the province/district and coordinates first, then feed those coordinates into directions.

When not to build this yourself

Honestly: Google Directions and Goong Directions are both mature and rock-solid — if you already pay for one and you're happy, there's no need to switch. GoGoDuk's edge for this problem is being free forever plus a motorbike routing profile tuned for Vietnamese roads, so distance (and therefore the fee) matches how riders actually travel. Note the car profile isn't live yet; if your fleet is cars, wait for that update.

Wrapping up

The whole formula reduces to: call /v1/directions → read distance.value (meters) and duration.value (seconds) → feed them into a tiered fee function, add a time surcharge if you need one, and handle ESTIMATED routes separately.

Questions, or want to show off what you built? Join our Telegram support group.

Want to use GoGoDuk?

Free forever — 100 requests/day per account, no credit card. Higher limits on request.

Sign up →