Documentation

Carriers (agents and zones)

Drivers, zones, and the dispatch model that hands jobs to the right agent.

Logistics doesn't integrate with parcel carriers (UPS, FedEx, USPS, DHL) — that's the Storefront's shipping module. What this module does manage is your own carrier network: the drivers, riders, and couriers (collectively "agents") who pick up and drop off jobs. They register through the driver app, take a job, and run it to completion.

For parcel carrier integration in e-commerce orders, see Storefront shipping.

Agent endpoints

GET/logistics/delivery/agentsJWT
GET/logistics/delivery/agents/onlineJWT
GET/logistics/delivery/agents/:agentIdJWT
PUT/logistics/delivery/agents/:agentId/locationJWT
PUT/logistics/delivery/agents/:agentId/availabilityJWT
PUT/logistics/delivery/agents/:agentId/approveJWT
PUT/logistics/delivery/agents/:agentId/suspendJWT
PUT/logistics/delivery/agents/:agentId/rejectJWT
GET/logistics/delivery/agents/:agentId/available-jobsJWT

Driver self-service

GET/client/logistics/initJWT
POST/client/logistics/registerJWT
GET/client/logistics/meJWT
PUT/client/logistics/availabilityJWT
PUT/client/logistics/locationJWT

Driver registration

A new driver registers via /client/logistics/register. The record is created in pending status; an admin reviews and approves before the driver can take jobs.

await fetch('/client/logistics/register', {
  method: 'POST',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    firstName: 'Sam',
    lastName: 'Park',
    email: '[email protected]',
    phone: '+1 555 0123',
    vehicleType: 'car',                    // 'bike' | 'car' | 'van' | 'truck'
    licenseNumber: '...',
    licensePlate: '...',
    homeZone: 'downtown',
    documents: { driversLicense: '...', insurance: '...' },
  }),
});

Staff approve through PUT /logistics/delivery/agents/:agentId/approve. Suspending and rejecting follow the same shape with appropriate audit reason.

Online status and location

A driver's app calls these on a heartbeat to keep dispatch informed:

// Toggle online / available
await fetch('/client/logistics/availability', {
  method: 'PUT',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    online: true,
    available: true,                       // false = on a job
  }),
});

// Push location every 10–30 seconds while online
await fetch('/client/logistics/location', {
  method: 'PUT',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    lat: 40.7128,
    lng: -74.006,
    heading: 90,
    speed: 12.5,
  }),
});

Location updates power both the tracking endpoint and the dispatcher's nearest-agent calculation.

Zones

Zones are named geographic areas. Each zone has a polygon (or simpler bounding shape) and a config for surge multipliers, agent caps, and accepted vehicle types.

GET/logistics/delivery/zonesJWT
GET/logistics/delivery/zones/:zoneNameJWT
GET/logistics/delivery/zones/lookupJWT
// Find the zone for a coordinate
const { zone } = await fetch(
  '/logistics/delivery/zones/lookup?lat=40.7128&lng=-74.006',
  { headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` } }
).then(r => r.json());

Zones are seeded through the data API (POST /repository/create/delivery_zone/create) — there isn't a dedicated zone-mgmt controller. Drivers carry a homeZone and can be filtered by it.

Dispatch — broadcast, offer, assign

Three ways to put a job in front of an agent:

Broadcast

Open the job to any qualifying online agent. First-come, first-served.

await fetch(`/logistics/delivery/jobs/${jobId}/broadcast`, {
  method: 'PUT',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
});

The job appears in GET /client/logistics/jobs/available for every eligible agent (vehicle type matches, in zone, online, available). The first to accept wins.

Offer

Push the job to a specific agent first. They get a window to accept or reject; if rejected or the timer expires, fall back to broadcast.

await fetch(`/logistics/delivery/jobs/${jobId}/offer`, {
  method: 'PUT',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ agentId: 'agent-xyz' }),
});

Direct assign

Skip the agent's accept/reject — staff assigns the job and the agent is notified to start. Use for VIP agents or back-office workflows.

await fetch(`/logistics/delivery/jobs/${jobId}/assign`, {
  method: 'PUT',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ agentId: 'agent-xyz' }),
});

Targeted alerts

Push a notification (in-app + push) to a curated set of agents — by id, by zone, by online state, or within a radius:

POST/logistics/delivery/jobs/:jobId/alert-agentsJWT
await fetch(`/logistics/delivery/jobs/${jobId}/alert-agents`, {
  method: 'POST',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    online: true,
    radius: { lat: 40.7128, lng: -74.006, miles: 3 },
  }),
});

This is most useful when a broadcast hasn't been picked up yet — manually nudge nearby drivers without changing the job's dispatch state.

Performance stats

Each agent record carries denormalised stats — completed jobs, average rating, on-time rate, total earnings. Recompute manually if you suspect drift:

PUT/logistics/delivery/agents/:agentId/recalculate-performanceJWT

This walks the agent's full job history and rewrites the stats block from source.

Drivers and customers share the same JWT scheme — the role is determined by which collection the principal record lives in (delivery_agent vs regular customer). The driver app should confirm GET /client/logistics/me returns 200 before showing driver-only screens.