N-Ways
Developer docsDelivery tracking page

Sample: delivery tracking page

The page your customer opens from the “Your order is on its way” message: the rider, the road to their door, and when they’ll arrive.

On this page

When an order leaves, your customer gets a message: “Your order is on its way”. The link opens this page: the rider on the map, the drop-off, and the arrival time, updated every few seconds.

Tip:

No code needed? Every delivery already has a ready-made tracking page: send the task’s trackingUrl (https://nways.ng/t/…). Build your own page, as below, when you want it on your own site, in your colours, next to the order details. Tracking links

How it fits together

  1. Your rider app sends positions to fleet tracking, and N-Ways keeps the delivery’s arrival time up to date.
  2. Your server reads the delivery and its vehicle with a server key (fleet keys never go in web pages), and answers your page with only what the customer should see.
  3. The page draws it with the Maps SDK, using a website key restricted to your site and to Maps.

The server

Node.js 18 or later, nothing to install. It serves the page and /track/{order}.json.

server.mjs
// node server.mjs   (NWAYS_SERVER_KEY in the environment)
import { readFile } from 'node:fs/promises'
import { createServer } from 'node:http'

const KEY = process.env.NWAYS_SERVER_KEY
const fleet = async (path) => {
  const res = await fetch('https://nways.ng/maps/v1/fleet' + path, { headers: { 'X-NWays-Api-Key': KEY } })
  const data = await res.json()
  if (!res.ok) throw Object.assign(new Error(data.error?.message), { status: res.status, reason: data.error?.reason })
  return data
}

// What the customer may see about their order: no rider name, phone or history.
async function tracking(orderId) {
  const task = await fleet(`/tasks/${encodeURIComponent(orderId)}`)
  const vehicle = task.vehicleId ? await fleet(`/vehicles/${encodeURIComponent(task.vehicleId)}`) : null
  const live = ['assigned', 'picked_up', 'arriving'].includes(task.status)
  return {
    status: task.status,
    dropoff: { lat: task.dropoff.lat, lng: task.dropoff.lng },
    eta: task.eta ? { at: task.eta.at, minutes: Math.round(task.eta.durationS / 60) } : null,
    rider: live && vehicle?.online && vehicle.lastLocation ? { lat: vehicle.lastLocation.lat, lng: vehicle.lastLocation.lng } : null,
  }
}

createServer(async (req, res) => {
  const m = req.url.match(/^\/track\/([\w.:-]{1,64})\.json$/)
  try {
    if (m) {
      res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
      return res.end(JSON.stringify(await tracking(m[1])))
    }
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
    res.end(await readFile(new URL('./track.html', import.meta.url)))
  } catch (e) {
    res.writeHead(e.reason === 'TASK_NOT_FOUND' ? 404 : 502, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ error: e.reason ?? 'ERROR' }))
  }
}).listen(8080, () => console.log('http://localhost:8080/?order=order-1041'))

The page

track.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="robots" content="noindex" />
    <title>Your delivery</title>
    <link rel="stylesheet" href="https://nways.ng/sdk/v1/maplibre-gl.css" />
    <style>
      body { margin: 0; font: 16px/1.4 system-ui, sans-serif }
      #map { height: 70vh }
      #info { padding: 16px }
      #info b { font-size: 1.5em; display: block }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <div id="info" aria-live="polite">Loading…</div>
    <script type="module">
      import { createMap, maplibregl } from 'https://nways.ng/sdk/v1/nways-maps.standalone.js'

      const order = new URLSearchParams(location.search).get('order')
      const info = document.getElementById('info')
      const map = createMap({ container: 'map', key: 'YOUR_WEBSITE_KEY', zoom: 14 })
      const home = new maplibregl.Marker({ color: '#0b7a3e' })
      const rider = new maplibregl.Marker({ color: '#1a73e8' })
      const WORDS = { assigned: 'Your rider is on the way to collect your order', picked_up: 'On its way to you', arriving: 'Arriving now', completed: 'Delivered', failed: 'Delivery failed', cancelled: 'Cancelled', created: 'Being prepared' }
      let framed = false

      async function update() {
        const res = await fetch(`/track/${encodeURIComponent(order)}.json`)
        if (!res.ok) return void (info.textContent = 'We can’t find this delivery.')
        const t = await res.json()
        home.setLngLat([t.dropoff.lng, t.dropoff.lat]).addTo(map)
        if (t.rider) rider.setLngLat([t.rider.lng, t.rider.lat]).addTo(map)
        else rider.remove()
        if (!framed) {
          const box = new maplibregl.LngLatBounds([t.dropoff.lng, t.dropoff.lat], [t.dropoff.lng, t.dropoff.lat])
          if (t.rider) box.extend([t.rider.lng, t.rider.lat])
          map.fitBounds(box, { padding: 80, maxZoom: 16, duration: 0 })
          framed = true
        }
        const when = t.eta ? new Date(t.eta.at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) : null
        info.innerHTML = ''
        info.append(
          Object.assign(document.createElement('b'), { textContent: when ? `Arriving about ${when}` : (WORDS[t.status] ?? t.status) }),
          t.eta ? `${WORDS[t.status] ?? ''} · ${t.eta.minutes} min` : '',
        )
        if (['completed', 'failed', 'cancelled'].includes(t.status)) clearInterval(timer)
      }

      const timer = setInterval(update, 10000)
      update()
    </script>
  </body>
</html>

Notes

  • Two keys. NWAYS_SERVER_KEY is restricted to your server’s IP addresses and to Fleet. YOUR_WEBSITE_KEY is restricted to your website and to Maps. Each createMap is one map load.
  • Privacy. The page shows the rider only while the delivery is under way and the vehicle is online, and never their name or phone number. Don’t log your customers’ positions in the page.
  • Faster updates. Poll every 10 seconds, as here, or push changes to the page from your server when webhooks arrive (task.arriving, task.completed).
  • French. createMap({ lang: 'fr' }) labels the map in French; translate the page’s words to match.