Files
doormile_backend/docs/miler-app-api.md
Suriya d85d5571b8 feat: rider visibility and tracking for the express console
A client login could list its own bookings but had no way to see what its
riders were actually doing. jupiter's console gave them getridersummary and
the rider/delivery logs; Doormile records all of it and exposed none of it.

New console endpoints, all tenant-scoped:
  GET /admin/milers/summary        roster with live state + range totals
  GET /admin/milers/:id/logs       GPS trail from the Redis telemetry index
  GET /admin/milers/:id/activity   one rider's assignments, duty and breaks
  GET /admin/consignments/:id/logs event history + telemetry + proof
  GET /admin/bookings/:id/track    booking -> assignments -> parcel -> proof

Also closes a rider IDOR: GetMilers scoped the roster to the caller's own
fleet, but reading, editing, blocking, notifying or assigning a vehicle to a
single rider by id did not, so a client login could walk the whole network's
riders by incrementing the id. All five now go through assertMilerAccess.

And the client dashboard no longer reports milers/customers/exceptions as
zero — those have no tenant column, so they are counted through appusers,
bookings and consignments respectively.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 11:41:02 +05:30

9.0 KiB

Doormile Miler App — API reference

The rider-app surface only (/miler/*). 38 routes: 3 auth + 35 authenticated. Base URL https://api.doormile.com/api/v1.

This supersedes "Miler App API Contract v1.0" where the two disagree — several shapes in that doc never matched the code. The known mismatches are called out inline below.


Auth

Two-step: phone → PIN. configid is 1001 — that's the partition riders live in. It defaults to 1001 if omitted, but a miler row created without it can never log in, so the console must set it at creation time.

POST /miler/login
{ "phone": "9876543210", "configid": 1001 }
→ { success, message: "PIN verification required", phone }
   404 if no account, 403 if not role 5 or not Active

POST /miler/verify-pin
{ "phone": "9876543210", "pin": "1234", "configid": 1001, "device_token": "fcm..." }
→ { success, token, user: { userid, authname, email, contactno, profile: {…MilerProfile…} } }

The verify-pin response has no data key — the fields the old contract doc listed as flat (displayname, hubid, availabilitystatus, rating) live under user.profile.

Send the token as Authorization: Bearer <token> on everything else. Role 5 is enforced; an admin token gets 401 here.

PIN reset is not self-service

POST /miler/reset-pin        ← requires an ADMIN token (roles 1/3/4)
{ "phone": "9876543210", "new_pin": "1234", "configid": 1001 }

It sits under /miler but it is a console/ops operation. It was previously open, and reset-pin → verify-pin took over any rider account with nothing but a phone number. The app must not call this; route rider PIN resets through ops.

Credential endpoints share a 10/min rate limit.


Profile & device

Method Path Body
GET /miler/profile
PUT /miler/profile { displayname, profilephotourl, defaultvehicletype, phone }
PUT /miler/device-token { "device_token": "..." } — snake_case

Location & availability

Method Path Body
PUT /miler/location { latitude, longitude, pincode, speed, heading }
PUT /miler/availability { "status": "Available" }

PUT /location writes Redis only — a SET plus a GEOADD into milers:locations, which is the index dispatch searches (10km radius, nearest 10). No NATS publish, despite what the old doc claimed. speed and heading are accepted and reach the telemetry log; they used to be silently dropped.

PUT /availability accepts either status or availabilitystatus — the doc told the Flutter side to send the second, the code only read the first, so both are honoured now rather than picking a winner.

Valid statuses: Offline, Available, Assigned, On_Pickup, At_Customer, Picked_Up, On_Delivery, Break, Blocked. Note it's Break, not On_Break.

Duty & breaks

Method Path Body
POST /miler/duty/start { lat, lon }
PUT /miler/duty/end
GET /miler/duty/current
POST /miler/breaks/start { "breaktype": "Lunch" }
PUT /miler/breaks/end

Ordering is enforced: starting duty twice returns "already on duty, end current duty first"; a break without duty returns "not on duty".

Assignments

Method Path Body
GET /miler/assignments the rider's own queue
GET /miler/assignments/:id
POST /miler/assignments/:id/accept
POST /miler/assignments/:id/reject { "reason": "too far" }

The pickup flow

In order, all keyed on :bookingid:

Step Method Path Body
1 POST /miler/bookings/:bookingid/reached
2 POST /miler/bookings/:bookingid/parcel { "parcels": [{ parcel_id, weight, length, width, height }] }
3 POST /miler/bookings/:bookingid/payment { amount, paymentmode, transactionref }
4 POST /miler/bookings/:bookingid/pickup-complete

Escape hatches:

Method Path Notes
POST /miler/bookings/:bookingid/vehicle-required params are query strings: ?type=truck&reason=...
POST /miler/bookings/:bookingid/cancel { "reason": "..." } — refused once picked up

paymentmode: Cash, UPI, Card, Wallet. Amount must be > 0.

pickup-complete is the pivot. It converts the booking into a consignment, recomputes chargeable weight from the parcel dimensions the rider entered in step 2, and decides routing: if the pickup and delivery pincodes share a 3-digit prefix it's hyperlocal and the consignment goes straight to Out_for_Delivery in the rider's hands. Otherwise it routes via the hub. The consignment inherits the booking's tenant, not the rider's.

Delivery

Method Path Body
POST /miler/consignments/:id/deliver { deliveredtoname, otp, photourl, receiversignatureurl, lat, lon }
POST /miler/consignments/:id/skip { reason, lat, lon }
  • deliveredtoname is required. reason is required on skip.
  • The consignment must be Out_for_Delivery or both return 400.
  • otp is only required when the tenant has requiredeliveryotp on. It's off by default, and off for DailyGrubs. When it is on, the OTP is checked server-side — a non-empty string is no longer enough.
  • lat/lon should be the actual delivery point: deliver computes riderkms from the pickup coords by haversine and writes it with ridercharges (the tenant's order amount, passed through from the booking's finalprice) onto the earnings record.
  • skip bumps attemptcount rather than failing the consignment.

Bookings & earnings

Method Path Query
GET /miler/bookings ?status=&date=YYYY-MM-DD
GET /miler/earnings ?period=daily|weekly|monthly&date=YYYY-MM-DD

bonuspoints stays zero — nothing writes it yet. That's known and deliberate.

Telemetry (Redis-backed, high frequency)

Method Path Body
POST /miler/logs one MilerLog
GET /miler/logs
POST /miler/status { "status": "Available" }
GET /miler/status
POST /miler/consignments/logs a JSON array of ConsignmentLog
GET /miler/consignments/logs/:consignmentid
GET /miler/consignments/userlogs/:userid must be your own userid

Lat/long/speed/heading/battery on these are strings, not numbers. Sending numbers fails to parse.

POST /miler/logs
{
  "logdate": "2026-08-06 14:32:10",   // YYYY-MM-DD HH:MM:SS, IST
  "latitude": "11.0168", "longitude": "76.9558",
  "speed": "24.5", "heading": "180", "accuracy": "8",
  "status": "On_Delivery", "orderid": "25",
  "battery": "72", "is_charging": false,
  "connection": "4G", "location_service": "enabled", "is_background": true
}
POST /miler/consignments/logs
[ { "consignmentid": 25, "logdate": "2026-08-06 14:32:10",
    "latitude": "11.0168", "longitude": "76.9558",
    "speed": "24.5", "heading": "180",
    "status": "Out_for_Delivery", "remarks": "", "battery": "72",
    "is_background": true } ]

Do not send userid in these bodies. All three used to read the rider identity from the request body, which let any logged-in rider write another rider's GPS into the dispatch index. Identity now comes from the token and a body userid is ignored; /userlogs/:userid rejects anyone else's id.

Redis is never the system of record here — a flush loses telemetry, not business state.

Notifications & support

Method Path Body
GET /miler/notifications
PATCH /miler/notifications/:id/read stub — see below
POST /miler/support { subject, description }
GET /miler/support

Notifications are synthesized fresh from BookingAssignment rows on every GET, and id is just the array index. PATCH .../read returns {success: true} without persisting anything, because there's no notifications table with read state. Read state cannot stick between calls until that table exists — don't build a UI that depends on it.


Conventions

  • Envelope: { "success": true, "data": ... }; failures are { "success": false, "message": "..." }.
  • Rate limits: 300/min per IP globally, 10/min across login/verify-pin/ reset-pin.
  • Timestamps are IST wall-clock. Send YYYY-MM-DD HH:MM:SS on telemetry, YYYY-MM-DD on date filters.
  • Status enums — booking: Pending_Pickup, Created, Miler_Assigned, Pickup_Scheduled, Picked_Up, Converted_To_Consignment, Cancelled. Consignment: Created, Inwarded_at_Hub, Tripsheet_Loaded, In_Transit, Out_for_Delivery, Delivered, RTO_Initiated, Returned_to_Sender, Missing, Damaged. Assignment: Assigned, Accepted, Rejected, Reassigned, Completed, Cancelled.

Known gaps

  1. PATCH /notifications/:id/read is a stub — needs a real table, schema not decided.
  2. bonuspoints is never written.
  3. assignments/:id/reject and bookings/:id/vehicle-required have never had a real request against them.