Documentation

Shipments

Create delivery jobs and walk them through the pickup → dropoff lifecycle.

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

POST/client/logistics/quoteJWT
POST/client/logistics/quote/validatedJWT
POST/client/logistics/jobsJWT
POST/client/logistics/ordersJWT
GET/client/logistics/ordersJWT
PUT/client/logistics/orders/:jobId/cancelJWT
GET/client/logistics/orders/:jobId/trackJWT

Staff

POST/logistics/delivery/jobsJWT
GET/logistics/delivery/jobsJWT
GET/logistics/delivery/jobs/:jobIdJWT
PUT/logistics/delivery/jobs/:jobId/cancelJWT
PUT/logistics/delivery/jobs/:jobId/failJWT
PUT/logistics/delivery/jobs/:jobId/pricingJWT

Driver

GET/client/logistics/jobsJWT
GET/client/logistics/jobs/availableJWT
PUT/client/logistics/jobs/:jobId/acceptJWT
PUT/client/logistics/jobs/:jobId/rejectJWT
PUT/client/logistics/jobs/:jobId/start-pickupJWT
PUT/client/logistics/jobs/:jobId/arrive-pickupJWT
PUT/client/logistics/jobs/:jobId/complete-pickupJWT
PUT/client/logistics/jobs/:jobId/start-dropoffJWT
PUT/client/logistics/jobs/:jobId/arrive-dropoffJWT
PUT/client/logistics/jobs/:jobId/complete-dropoffJWT
PUT/client/logistics/jobs/:jobId/completeJWT

Quote

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:

POST/logistics/delivery/jobs/:jobId/adjustmentsJWT
await 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.