Saves stop and product updates for one or more stops on a route. Handles stop status, product counts, OSD entries, driver comments, and signatures.
Endpoint: PUT /api/route/stop
Authentication: Required: headers: employee_id, api_token
Note (Route closure: behavior differs by environment as of v23): Whether to enable arrival-gating in production is an ongoing internal decision (still unresolved as of this writing), not a bug: both paths below are current and intentional for now.
DB_LIBRARIES other than DVXDEVFIL): legacy behavior: submitting stop id: 999 with updates.status: "D" closes the route (GROUTEC.ROUTEPOD = 'C') immediately. No arrival check, no timer.DB_LIBRARIES=DVXDEVFIL): route closure is gated on warehouse arrival. Submitting stop 999 with arrived: true sets GROUTEC.ROUTERTN = 'Y'. When stop 999 is later submitted with updates.status: "D": if arrival was recorded, the route closes immediately; otherwise a 10-minute server-side timer starts (keyed per route, deduped against concurrent submits) and force-closes the route when it fires even without a confirmed arrival.Note (OSD): Both product.updates.osd_data and product.action.updates.osd_data use the same grouped format: [{ osd_id, osd_code, data: [{ osd_parent, comment, quantity, images_id, barcodes, quantity_type }] }]. barcodes is a single barcode string per entry (empty string if none). quantity_type v23 is "units", "cases", or omitted/null: written to the PDQTYIS column. osd_id (group-level) and osd_parent (entry-level) v23 are opaque char(6) values written to PDOSDID/PDOSDPAR and echoed back unchanged on GET: the API only stores and recalls them, it does not interpret them. Product-level OSD writes PDIDCODE = product_id, PDUPCCD = barcodes, PDOSDITMZ = 'N'. Action (itemized) OSD writes PDIDCODE = product_id, PDUPCCD = '', PDOSDITMZ = 'Y'; barcodes in data entries are not persisted.
Note (Overage Items): Items found at the stop that are not on the manifest go in stop.updates.overage_items: [{ barcode, osd_code, quantity, comment, images_id }]: no quantity_type on this shape. Each entry writes one GPODOSD row (PDIDCODE = '', PDUPCCD = barcode, PDOSDITMZ=N). On GET, rows with an empty PDIDCODE are returned in stop.updates.overage_items.
Note (Delivery Email): If the stop is non-warehouse (id !== 999), updates.status is "D", and the route is flagged GROUTEC.ROUTEPPOD = 'Y', a delivery summary email is sent automatically after all writes complete. See the email preview tool below.
Note (Training mode, v23+): Send "training": true at the top level of the body to run the request without changing any data. Auth, StopGuard, reads, the response shape and logging all run normally.
22001/22003).rows_updated is what the real write would have matched. A write that would fail appears in update_log exactly like a real failure.training: { writes_validated, writes_failed, writes: [{ kind, table, rows, sql, error }] }.true turns it on. v22 and earlier ignore it.Note (Stop Guard, v22+): Before anything is written, the request is checked by lib/StopGuard. These checks never reject the request.
employee_id is not ROUTEDRV, ROUTEDRV2 or ROUTEAST on the route for that week in GROUTEC, the stops are still written and one extra entry is added to update_log: { "success": true, "level": "guard", "facility", "route_id", "employee_id", "not_assigned": true, "error": null }. This entry counts toward the "N updates performed" message. If the assignment lookup fails, it is logged and the write goes ahead.id sent more than once with identical contents is written once. No update_log entry is added.id sent more than once with different contents is skipped entirely (nothing is written for that stop). It gets { "success": false, "level": "stop", "stop_id", "error": "Conflicting duplicate stop in request — nothing written for this stop." }. Other stops in the request are written normally.Request body:
{
"id": "17140",
"facility": "011",
"week_end": "20251031",
"stops": [
{
"id": 258,
"arrived": false,
"updates": {
"status": "D",
"followup_reason_code": "",
"start_delivery_time": "2025-10-31T10:00:00Z",
"end_delivery_time": "2025-10-31T10:15:00Z",
"delivery_latitude": "41.8781",
"delivery_longitude": "-87.6298",
"delivered_by_driver_id": "T0048",
"store_emp_id": "EMP001",
"store_emp_name": "JOHN DOE",
"driver_comments": "Left at dock",
"skip_refuse_reason_code": "",
"skip_refuse_timestamp": "",
"skip_refuse_driver_comment": "",
"signature_id": "",
"osd_emails": [],
"overage_items": [
{ "barcode": "012345678901", "osd_code": "85", "quantity": 1, "comment": "", "images_id": "" }
]
},
"products": [
{
"id": "038976",
"invoice": "0806591",
"updates": {
"accounting_complete": false,
"delivered": 6,
"returned": 0,
"delivered_pallets": null,
"image_ids": null,
"osd_data": [
{
"osd_id": "ABCDEF",
"osd_code": 51,
"data": [
{ "osd_parent": "ABCDEF", "comment": "1 bag missing", "quantity": 1, "images_id": "", "barcodes": "00016000148710", "quantity_type": "units" },
{ "osd_parent": null, "comment": "1 damaged box", "quantity": 1, "images_id": "", "barcodes": "016000502789", "quantity_type": null }
]
}
]
},
"action": {
"updates": { "delivered": 0, "osd_data": [] },
"type": null,
"open": false,
"open_action": null,
"item_name": null,
"quantity": 7
}
},
{
"id": "9001",
"invoice": "0806591",
"updates": {
"accounting_complete": false,
"delivered": 10,
"returned": 0,
"delivered_pallets": null,
"image_ids": null,
"osd_data": []
},
"action": {
"updates": { "delivered": 10, "osd_data": [] },
"type": "count",
"open": false,
"open_action": null,
"item_name": null,
"quantity": 10
}
}
],
"collections": [
{
"id": "9009",
"invoice": "0806591",
"updates": {
"returned": 0,
"accounting_complete": false,
"image_ids": null,
"osd_data": []
},
"action": {
"updates": { "delivered": 0, "osd_data": [] },
"type": "collect",
"open": false,
"open_action": null,
"item_name": null,
"quantity": 0
}
}
]
}
]
}
Result:
...
Renders the delivery summary email's generated HTML directly in the response: no email is sent. Useful for checking layout/content without triggering a real send (which is otherwise a side effect of PUT /api/route/stop, not directly callable).
Endpoint: GET /api/route/stop/email-preview?facility=&route_id=&stop_id=&week_end=
Authentication: None
Marks a stop as skipped or refused.
Endpoint: PUT /api/route/stop/skip-refuse
Authentication: Required: headers: employee_id, api_token
Required body fields: route_id, stop_id
Result:
...