Documentation

Proof of delivery

Photo, signature, and timestamp captured at dropoff.

Proof of delivery (POD) is the evidence a job actually completed: a photo of the package on the doorstep, a signature from the recipient, and the wall-clock timestamp of completion. POD is captured by the driver app at the dropoff step and stored as an attachment on the job record.

Endpoints

POST/client/logistics/jobs/:jobId/imagesJWT
GET/client/logistics/jobs/:jobId/imagesJWT
POST/client/logistics/uploadJWT
POST/logistics/delivery/jobs/:jobId/imagesJWT
GET/logistics/delivery/jobs/:jobId/imagesJWT

Capturing POD

The simplest POD path: the driver completes dropoff and uploads images keyed to pod and signature types.

// 1. Upload the photo
const fd = new FormData();
fd.append('file', photoBlob, 'pod.jpg');
const { url } = await fetch('/client/logistics/upload', {
  method: 'POST',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
  body: fd,
}).then(r => r.json());

// 2. Attach to the job
await fetch(`/client/logistics/jobs/${jobId}/images`, {
  method: 'POST',
  headers: {
    orgid: 'my-org',
    Authorization: `Bearer ${jwt}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'pod',                       // 'pod' | 'signature' | 'damage' | 'other'
    url,
    capturedAt: new Date().toISOString(),
    location: { lat: 40.7128, lng: -74.006 },
    note: 'Left at front door',
  }),
});

// 3. Mark dropoff completed (writes the timestamp + transitions state)
await fetch(`/client/logistics/jobs/${jobId}/complete-dropoff`, {
  method: 'PUT',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
});

Image types

Each image carries a type so the customer-facing UI can render it correctly:

TypeUsed for
podStandard "delivered" photo
signatureRecipient signature, captured as PNG/JPEG
damageDriver-flagged damage at pickup or dropoff
otherFree-form attachment (warehouse receipt, gate access photo)

The same /images endpoint handles all four; only type changes.

Timestamps

POD timestamps come from three places on the job record:

{
  data: {
    completedAt: '2026-04-25T16:32:11Z',   // /complete-dropoff or /complete
    pod: {
      photoUrl: 'https://...',
      signatureUrl: 'https://...',
      capturedAt: '2026-04-25T16:31:48Z',  // when driver tapped capture
      location: { lat, lng },
    },
    timeline: [
      { event: 'dropoff_completed', at: '...', by: 'agent-id' },
    ],
  },
}

If the driver app captures POD locally and uploads later (poor signal at the dropoff), pod.capturedAt reflects the original capture moment, not the upload moment. The timeline entry uses the server-side completion time.

Reading POD on the customer side

A customer's track page calls GET /client/logistics/orders/:jobId/track (see Tracking) — the response includes the POD URLs once the job is complete. For older completed orders, fetch images directly:

const images = await fetch(`/client/logistics/jobs/${jobId}/images`, {
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
}).then(r => r.json());

// [{ type: 'pod', url, capturedAt, ... }, ...]

Storage

POD media goes through the standard AppEngine file provider — images live under the org's storage bucket, indexed by job id. Signed URLs are generated on read; raw S3 paths are not exposed to clients. Retention is policy-driven per org (configurable via the data layer) and defaults to 90 days.

Don't bake the URL into emails

The image URL returned is signed and expires. If you need the POD photo embedded in a delivery confirmation email, fetch a fresh signed URL at email render time rather than storing the URL in your template.

Required POD

Set requirements.requiresPhoto: true and/or requirements.requiresSignature: true on a job to make POD mandatory. Drivers can't transition to dropoff_completed without the matching attachment — the API rejects the call if POD is missing for a job that requires it.

{
  requirements: {
    vehicleType: 'car',
    requiresSignature: true,
    requiresPhoto: true,
    fragile: false,
  },
}