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>
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 } |
deliveredtonameis required.reasonis required on skip.- The consignment must be
Out_for_Deliveryor both return 400. otpis only required when the tenant hasrequiredeliveryotpon. 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/lonshould be the actual delivery point:delivercomputesriderkmsfrom the pickup coords by haversine and writes it withridercharges(the tenant's order amount, passed through from the booking'sfinalprice) onto the earnings record.skipbumpsattemptcountrather 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:SSon telemetry,YYYY-MM-DDon 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
PATCH /notifications/:id/readis a stub — needs a real table, schema not decided.bonuspointsis never written.assignments/:id/rejectandbookings/:id/vehicle-requiredhave never had a real request against them.