11 KiB
Reverse logistics (Return to sender): rider app integration
This is the backend contract the Miler (rider) app needs in order to support returns, also called RTO ("return to origin"). It lists what the server sends, what the app should show, the one new endpoint, and the order to roll it out in.
Base URL https://api.doormile.com/api/v1. Every endpoint here uses the normal
rider login (Authorization: Bearer <miler token>).
Summary for the app team. A parcel can now be sent back to its sender instead of being delivered. While that is happening, the parcel's consignment status is
RTO_Initiated, but the booking status does not change. Today the app decides what to show from the booking status, so a parcel being returned still looks deliverable. Tapping Deliver or Skip then returns a 400. The app must read the per-parcel fields below and show a "Return to sender" stop instead.
1. What happens to a parcel
Collected_By_Miler / Out_for_Delivery
│ ops press "Return to sender" in the console,
│ or automatically after 3 failed delivery attempts (skips)
▼
RTO_Initiated ──── ops "Re-attempt delivery" ───▶ back to Out_for_Delivery
│ (or wherever it was)
│ rider hands it back to the sender ← NEW: POST …/return-complete
│ (or ops press "Mark returned")
▼
Returned_to_Sender (final; the rider's job on it is closed)
- Where it goes: back to the sender, which is the parcel's pickup
point. The server sends the coordinates (
return_to, see §3). The app never chooses the destination. - Who starts it: ops, from the console, or the server automatically on the 3rd failed attempt. The rider never starts a return.
- Who finishes it: the rider, through the new endpoint once the feature flag is on (§6), or ops from the console.
Wire values (spell exactly like this)
| Thing | Value |
|---|---|
| Consignment status while returning | RTO_Initiated |
| Consignment status once returned | Returned_to_Sender |
next_action while returning |
return_to_sender (only when the flag is on, §6) |
next_action once returned |
none |
return_to.type |
sender |
Push data.type |
rto |
2. The feature flag (server side)
MILER_RTO_FLOW_ENABLED is an environment variable on the server, off by
default. It is read on every request.
| flag off (today) | flag on (after your release) | |
|---|---|---|
next_action for a parcel being returned |
none |
return_to_sender |
can_return |
false |
true |
POST /miler/consignments/:id/return-complete |
403 RTO_FLOW_DISABLED |
works |
| Who closes the return | ops, in the console | the rider (or ops) |
returning, return_reason and return_to are sent in both states, so the
app can show the return as soon as your build ships. That is before the flag
goes on.
3. Reading a parcel: GET /miler/consignments/:consignmentid
Unchanged fields stay as they were. Four fields are new and are additive, so older builds ignore them.
| Field | Type | Meaning |
|---|---|---|
returning |
bool | true while the parcel is being returned (status == "RTO_Initiated"). |
can_return |
bool | true when the rider may finish the return in the app (returning and flag on). Show the "Returned to sender" button only when this is true. |
return_reason |
string | Why it is being returned, e.g. "Receiver refused: gate locked". "" when not returning. |
return_to |
object | null | Where to take it. null when not returning. |
return_to:
{ "type": "sender", "latitude": 11.0168, "longitude": 76.9558, "pincode": "641001" }
Example response for a parcel being returned (flag on):
{
"success": true,
"data": {
"consignmentid": 9103,
"trackingno": "DMX09103",
"status": "RTO_Initiated",
"attemptcount": 0,
"next_action": "return_to_sender",
"returning": true,
"can_return": true,
"return_reason": "Address not found: No such door number",
"return_to": { "type": "sender", "latitude": 11.0168, "longitude": 76.9558, "pincode": "641001" },
"can_deliver": false,
"can_skip": false,
"can_start_delivery": false,
"can_inward_at_hub": false,
"delivered": false,
"out_for_delivery": false,
"collected": false,
"paymentmode": "",
"codamount": 0,
"codcollected": 0,
"next_hub": null,
"inwardedat": null
}
}
With the flag off, the same parcel has "next_action": "none" and
"can_return": false. Everything else is the same.
Note that can_deliver, can_skip and can_start_delivery are all false
while returning. If the app already uses these flags to enable its buttons, the
buttons disable themselves correctly.
4. The queue: GET /miler/bookings
Each stop already carries consignmentid, consignmentstatus, trackingno,
next_action and next_hub. Nothing new is added here. A stop being returned
shows:
consignmentstatus: "RTO_Initiated"next_action: "return_to_sender"(flag on) or"none"(flag off)- the booking
statusunchanged (e.g.Converted_To_Consignment)
This is the important app change: ApiConfig.legacyStatusFromNew maps
Converted_To_Consignment to picked. A parcel being returned therefore
renders as a normal delivery today. Before mapping a stop to a delivery card,
check consignmentstatus / next_action:
consignmentstatus |
next_action |
Show |
|---|---|---|
RTO_Initiated |
return_to_sender |
Return to sender card: navigate to return_to, "Returned to sender" button |
RTO_Initiated |
none |
Being returned: no Deliver/Skip buttons; text such as "Return to sender. Ops will close this." |
Returned_to_Sender |
none |
Done: move to history like a delivered stop |
To get return_to and return_reason for a stop, read
GET /miler/consignments/:consignmentid. A push (below) is also a good moment
to refresh.
5. Finishing a return: POST /miler/consignments/:id/return-complete (NEW)
The rider has handed the parcel back to the sender.
Headers: Authorization: Bearer <token>, Content-Type: application/json, and
Idempotency-Key: <uuid> (recommended; see below).
Request:
{
"lat": 11.0168,
"lon": 76.9558,
"receivedby": "Acme kitchen manager",
"photourl": "https://…/proof.jpg"
}
| Field | Required | Notes |
|---|---|---|
lat, lon |
send them | Rider's position at handover. Stored in the parcel history. |
receivedby |
optional | Who at the sender took it back. |
photourl |
optional | Proof photo. Upload it the same way as delivery proof (POST /miler/uploads/sign, then use the URL). |
Success 200:
{ "success": true, "data": { "consignmentid": 9103, "status": "Returned_to_Sender", "next_action": "none" } }
What the server does on success:
- parcel →
Returned_to_Sender, with the return time stored; - a history row:
Returned to sender by rider at (lat, lon), received by …, photo …; - the rider's assignment on that booking →
Completed.
The rider's job on that parcel is then closed, so it no longer blocks End duty.
Errors. Codes follow the existing miler format,
{"success": false, "code": "...", "message": "..."}. Plain 400/404/500
responses have no code and only carry message.
| HTTP | code |
When | App should |
|---|---|---|---|
| 403 | RTO_FLOW_DISABLED |
The server flag is off | Hide the button. This should not happen if you check can_return. |
| 400 | INVALID_STATE |
Parcel is not being returned (e.g. ops re-attempted it, or it was delivered) | Refresh the parcel and show its new state |
| 404 | — (message: "consignment not found") |
No such parcel | Refresh the queue |
| 404 | CONSIGNMENT_NOT_ASSIGNED |
Parcel isn't on this rider's bookings | Refresh the queue |
| 400 | — | Bad id or bad JSON body | Bug in the app |
| 500 | — | Server error, nothing was saved | Retry with the same Idempotency-Key |
Retries are safe. With the same Idempotency-Key, a successful response is
replayed for 24 h. Even without the key, a second call on a parcel that is
already Returned_to_Sender returns 200 and changes nothing.
6. Skip responses change on the 3rd attempt
POST /miler/consignments/:id/skip is unchanged in what it accepts. The
response status can now be RTO_Initiated:
{ "success": true, "data": { "consignmentid": 9102, "attemptcount": 3, "status": "RTO_Initiated" } }
That is the server starting the return automatically, by default on the 3rd
failed attempt (server setting RTO_AUTO_AFTER_ATTEMPTS; ops may set it to 0
to turn this off). After a skip, use the returned status. If it is
RTO_Initiated, switch the stop to the return card (§4) instead of keeping it
as a delivery to retry.
Also new: skip now works for the 2nd, 3rd … orders of a multi-drop pickup. It
used to answer CONSIGNMENT_NOT_ASSIGNED for every order after the first.
7. Push notification
When ops start a return (or it starts automatically), the rider holding the parcel gets:
| title | Return parcel to sender |
| body | Parcel DMX09103 is being returned to the sender. Do not attempt delivery. |
| data | { "type": "rto", "consignmentid": "9103" } (both strings) |
On type == "rto": refresh that consignment (§3) and the queue (§4). A rider
who already handed the parcel over at a base is not notified.
8. Rollout order
- Backend deployed with the flag off. Ops start and close returns from the
console; riders get the push. Until step 3, a return can leave a rider
unable to end duty until ops press "Mark returned". To avoid that, ops may
deploy with
RTO_AUTO_AFTER_ATTEMPTS=0. - App release: §4 (card per
consignmentstatus/next_action), §3 fields, §5 button behindcan_return, §6 skip handling, §7 push. - Flag on (
MILER_RTO_FLOW_ENABLED=true) once most riders have the new build. Riders finish their own returns, and automatic returns can be turned on.
Old builds keep working at every step. The new fields are additive, and with the flag off nothing new is required from the app.
9. App test checklist
Use a staging backend with MILER_RTO_FLOW_ENABLED=true.
- Ops start a return on a parcel the rider is carrying → push arrives → the stop turns into a Return to sender card with the reason; Deliver and Skip are gone.
- Navigation goes to
return_to(the sender), not to the receiver. - "Returned to sender" (with photo and receiver name) →
200→ the stop moves to history → End duty works. - Tap it twice or with no network, then retry → no error, one return.
- Skip the same parcel 3 times → the 3rd response has
status: RTO_Initiated→ the card switches to return. - Ops press "Re-attempt delivery" while the card is open → button tap gets
400 INVALID_STATE→ the app refreshes and shows the delivery again. - Flag off: the card shows "Being returned", with no button and no 403 shown to the rider.
- Multi-drop pickup: skip and return work on the 2nd and 3rd order.
Server code: controllers/consignmentReturn.go. The full plan, decisions and
test record are in krow_talent_app/docs/reverse-logistics-plan.md.