A job is a single unit of delivery work — one pickup, one or more dropoffs, with metadata about contents, scheduling, pricing, and the customer. The endpoints below let you create a job, list/find them, and run them through the status lifecycle.
Endpoints
Customer
/client/logistics/quoteJWT/client/logistics/quote/validatedJWT/client/logistics/jobsJWT/client/logistics/ordersJWT/client/logistics/ordersJWT/client/logistics/orders/:jobId/cancelJWT/client/logistics/orders/:jobId/trackJWTStaff
/logistics/delivery/jobsJWT/logistics/delivery/jobsJWT/logistics/delivery/jobs/:jobIdJWT/logistics/delivery/jobs/:jobId/cancelJWT/logistics/delivery/jobs/:jobId/failJWT/logistics/delivery/jobs/:jobId/pricingJWTDriver
/client/logistics/jobsJWT/client/logistics/jobs/availableJWT/client/logistics/jobs/:jobId/acceptJWT/client/logistics/jobs/:jobId/rejectJWT/client/logistics/jobs/:jobId/start-pickupJWT/client/logistics/jobs/:jobId/arrive-pickupJWT/client/logistics/jobs/:jobId/complete-pickupJWT/client/logistics/jobs/:jobId/start-dropoffJWT/client/logistics/jobs/:jobId/arrive-dropoffJWT/client/logistics/jobs/:jobId/complete-dropoffJWT/client/logistics/jobs/:jobId/completeJWTQuote
Customer apps call quote first to show price + ETA before booking. The validated variant geocodes addresses server-side and returns clean coordinates plus distance.
const quote = await fetch('/client/logistics/quote/validated', {
method: 'POST',
headers: {
orgid: 'my-org',
Authorization: `Bearer ${jwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
stops: [
{ type: 'pickup', location: { address: '123 Main St, NY' } },
{ type: 'dropoff', location: { address: '456 Side Ave, NY' } },
],
configName: 'standard',
}),
}).then(r => r.json());
// { price: 18.50, currency: 'USD', distance: 4.2, etaMinutes: 35, ... }
Creating a job
await fetch('/client/logistics/jobs', {
method: 'POST',
headers: {
orgid: 'my-org',
Authorization: `Bearer ${jwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customer: {
firstName: 'Alex',
lastName: 'Reyes',
email: '[email protected]',
phone: '+1 555 0100',
},
stops: [
{
type: 'pickup',
location: { address: '123 Main St, NY' },
contact: { name: 'Pickup Co', phone: '+1 555 0001' },
notes: 'Ring doorbell, parcel at reception',
},
{
type: 'dropoff',
location: { address: '456 Side Ave, NY' },
contact: { name: 'Alex Reyes', phone: '+1 555 0100' },
},
],
requirements: {
vehicleType: 'car', // 'bike' | 'car' | 'van' | 'truck'
requiresSignature: true,
requiresPhoto: true,
fragile: false,
},
scheduling: {
type: 'asap', // 'asap' | 'scheduled'
scheduledAt: undefined,
pickupWindow: undefined,
},
pricing: { /* server-computed if omitted */ },
notes: 'Internal staff note',
customerNotes: 'Note shown to customer',
}),
});
The job is created in pending status. From here the dispatcher (or the auto-broadcast logic) finds an agent.
Status lifecycle
pending
└─→ broadcast / offered / assigned
└─→ accepted
└─→ pickup_in_progress
├─→ pickup_arrived
└─→ pickup_completed
└─→ dropoff_in_progress
├─→ dropoff_arrived
└─→ dropoff_completed
└─→ completed
└─→ cancelled (terminal)
└─→ failed (terminal)
Each step is a separate endpoint on the driver controller. They're written as PUT calls because they mutate state on a single resource:
// Driver flow
await fetch(`/client/logistics/jobs/${jobId}/accept`, {
method: 'PUT',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
});
await fetch(`/client/logistics/jobs/${jobId}/start-pickup`, { method: 'PUT', ... });
await fetch(`/client/logistics/jobs/${jobId}/arrive-pickup`, { method: 'PUT', ... });
await fetch(`/client/logistics/jobs/${jobId}/complete-pickup`, { method: 'PUT', ... });
// ... and the dropoff equivalents
await fetch(`/client/logistics/jobs/${jobId}/complete`, { method: 'PUT', ... });
Cancel and fail
A customer can cancel a job before pickup (PUT /client/logistics/orders/:jobId/cancel); after pickup, cancellation falls back to the staff path. Drivers can fail a job (PUT /logistics/delivery/jobs/:jobId/fail) with a reason — used when delivery is genuinely impossible (recipient absent, address invalid).
await fetch(`/client/logistics/orders/${jobId}/cancel`, {
method: 'PUT',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: 'Customer no longer needs delivery' }),
});
Pricing adjustments
Staff can edit pricing on an in-flight job (e.g. add a long-route surcharge, apply a goodwill discount):
await fetch(`/logistics/delivery/jobs/${jobId}/pricing`, {
method: 'PUT',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
base: 12.0,
distance: 6.5,
surcharge: 2.0,
tax: 1.85,
total: 22.35,
}),
});
For finer-grained additive changes (one-line adjustments), use:
/logistics/delivery/jobs/:jobId/adjustmentsJWTawait fetch(`/logistics/delivery/jobs/${jobId}/adjustments`, {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'tip', // 'tip' | 'surcharge' | 'refund' | 'correction'
amount: 5,
reason: 'Customer tip',
actor: 'system',
}),
});
Listing and filtering
Customer side returns just their orders; staff side queries everything with filters:
const jobs = await fetch(
'/logistics/delivery/jobs?status=accepted&zone=downtown&page=1&pageSize=50',
{ headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` } }
).then(r => r.json());
The path /delivery/shipments/create mentioned in earlier specs maps to POST /logistics/delivery/jobs on the current build — "shipment" and "job" are interchangeable terms in the codebase, with "job" being canonical.