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
/client/logistics/jobs/:jobId/imagesJWT/client/logistics/jobs/:jobId/imagesJWT/client/logistics/uploadJWT/logistics/delivery/jobs/:jobId/imagesJWT/logistics/delivery/jobs/:jobId/imagesJWTCapturing 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:
| Type | Used for |
|---|---|
pod | Standard "delivered" photo |
signature | Recipient signature, captured as PNG/JPEG |
damage | Driver-flagged damage at pickup or dropoff |
other | Free-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.
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,
},
}