# 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 ` 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. ```jsonc 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 } ``` ```jsonc 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.