Tracking is what powers the "where's my driver" view on a customer's order page. It combines two data sources: the job's status transitions (from the driver hitting /start-pickup, /arrive-dropoff, etc.) and the driver's live location heartbeat. The customer endpoint returns both in a single shape.
Endpoints
/client/logistics/orders/:jobId/trackJWT/client/logistics/jobs/:jobId/trackingJWT/logistics/delivery/jobs/:jobId/trackingJWTCustomer track view
const track = await fetch(`/client/logistics/orders/${jobId}/track`, {
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
}).then(r => r.json());
// {
// jobId, status: 'dropoff_in_progress',
// stops: [
// { type: 'pickup', location: {...}, status: 'completed', completedAt: '...' },
// { type: 'dropoff', location: {...}, status: 'in_progress', etaMinutes: 8 }
// ],
// agent: {
// id, firstName, vehicleType, vehiclePlate,
// location: { lat, lng, heading, updatedAt },
// phone: '+1 555 0123', // masked
// rating: 4.8,
// },
// timeline: [
// { event: 'job_created', at: '...' },
// { event: 'agent_assigned', at: '...' },
// { event: 'pickup_started', at: '...' },
// ...
// ]
// }
The endpoint is auth-gated by the customer JWT — only the customer who placed the job can read it. To support guest tracking (no account), generate a signed track URL containing a one-off token; the front-end exchanges it on load and caches the JWT for the page.
Polling vs WebSocket
Out of the box, tracking is poll-based — the customer page hits /track every 10–15 seconds. There is no dedicated WebSocket gateway for delivery tracking in the current build; if you need push updates, listen to the delivery_job.updated event in the Automation module and forward the changes you care about over your own WS layer.
Updating tracking
The driver's app pushes per-job tracking updates with notes and ETA:
await fetch(`/client/logistics/jobs/${jobId}/tracking`, {
method: 'PUT',
headers: {
orgid: 'my-org',
Authorization: `Bearer ${jwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
status: 'pickup_in_progress',
note: 'Picking up package',
etaMinutes: 12,
location: { lat: 40.7128, lng: -74.006 },
}),
});
The shape merges into data.tracking on the job and the timeline entry is appended automatically. Most apps don't need to call this directly — the /start-pickup, /complete-pickup, etc. endpoints do it as a side effect.
Status events
Each status transition emits a domain event the rest of AppEngine can subscribe to:
| Event | When | Typical use |
|---|---|---|
delivery_job.created | Job created | Trigger autoassignment automation |
delivery_job.assigned | Agent picked or auto-assigned | Notify customer "your driver is on the way" |
delivery_job.pickup_started | Driver heading to pickup | Update merchant dashboard |
delivery_job.pickup_arrived | Driver at pickup point | Notify pickup contact |
delivery_job.pickup_completed | Package collected | "Out for delivery" customer email |
delivery_job.dropoff_completed | POD captured | Receipt, review request |
delivery_job.cancelled / failed | Terminal failures | Refund, retry workflows |
Hook these from the Automation module to fan out emails, SMS, or webhooks without writing code.
Carrier webhooks
For orgs running a hybrid model — own fleet plus parcel carrier — the Storefront's shipping integration handles carrier (UPS / FedEx / USPS / DHL) tracking webhooks under /storefront/shipping/webhook/*. Those webhooks update the order's shipment record with carrier status; they do not touch a Logistics delivery_job. Don't try to merge the two — they describe different work units.
Estimating ETA
The etaMinutes field is computed at quote time and updated as the driver moves. The default model is straight-line distance from the driver's current location to the next stop divided by an average speed for the vehicle type. For higher accuracy, plug a routing service into the DeliveryService.getRouteDistance extension point — see the source under /Users/imzee/projects/appengine/src/logistics/delivery.service.ts.
Tracking endpoints return the agent's masked phone number — never the raw line. If you need direct contact, use the in-job messaging endpoints (POST /client/logistics/jobs/:jobId/messages) which proxy through AppEngine without exposing personal numbers.