297 lines
12 KiB
Markdown
297 lines
12 KiB
Markdown
# Doormile Miler App — API reference
|
|
|
|
The rider-app surface only (`/miler/*`). 45 routes: 3 auth + 42 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 & uploads
|
|
|
|
| Method | Path | Body |
|
|
|---|---|---|
|
|
| GET | `/miler/profile` | Exposes `tenantname`, profile details, vehicle info |
|
|
| PUT | `/miler/profile` | `{ displayname, profilephotourl, defaultvehicletype, phone }` |
|
|
| PUT | `/miler/device-token` | `{ "device_token": "..." }` — snake_case |
|
|
| POST | `/miler/uploads/sign` | `{ "content_type": "image/jpeg", "kind": "pod" }` → `{ "upload_url": "...", "key": "..." }` |
|
|
|
|
## 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.
|
|
|
|
It returns `next_action` and, when the next leg is a base, `next_hub` with all
|
|
six fields (`id, name, address, pincode, latitude, longitude`) — the app never
|
|
picks a base itself. `consignment_id` is always present.
|
|
|
|
```jsonc
|
|
{ "consignment_id": 4821, "consignmentstatus": "Created",
|
|
"next_action": "inward_at_hub",
|
|
"next_hub": { "id": 1, "name": "Coimbatore Hub",
|
|
"address": "14 Avinashi Road, Peelamedu, Coimbatore",
|
|
"pincode": "641004", "latitude": 11.0272, "longitude": 76.9905 } }
|
|
```
|
|
|
|
## The base handover
|
|
|
|
```
|
|
POST /miler/consignments/:id/inward-at-hub Idempotency-Key supported
|
|
{ "hub_id": 1, "latitude": 11.0272, "longitude": 76.9905 }
|
|
→ { consignmentstatus: "Inwarded_at_Hub", inwardedat, hub, next_action,
|
|
already_inwarded }
|
|
```
|
|
|
|
The authoritative record that a rider handed a parcel in at a base. The body is
|
|
optional (it defaults to the base the parcel is routed to); `hubid`, `lat` and
|
|
`lon` are accepted as aliases. Answers with the resulting state, never a bare
|
|
200. A parcel already inwarded answers 200 with `already_inwarded: true`.
|
|
|
|
```
|
|
GET /miler/bases ?status=Active &applocationid=
|
|
```
|
|
|
|
Base master data on a rider token — the six fields per base, plus `distance_km`
|
|
and nearest-first ordering once the rider has reported a position.
|
|
`/admin/tenants/:id/locations` is a different dataset (a client's own sites) and
|
|
is closed to role 5 by design.
|
|
|
|
**Wording:** the wire says hub, the rider app says Base. Full contract and state
|
|
transitions in [`logistics-base-handover.md`](logistics-base-handover.md).
|
|
|
|
## 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` |
|
|
|
|
Every `/miler/bookings` row carries the leg and the pickup source, rebuilt from
|
|
server state on each read, so a poll or a cold restart needs no local cache:
|
|
|
|
| Field | Values |
|
|
|---|---|
|
|
| `next_action` | `pickup`, `inward_at_hub`, `start_delivery`, `deliver`, `handed_to_hub`, `none` |
|
|
| `next_hub` | the six base fields, or null when the next leg isn't a base |
|
|
| `pickup_source_type` | `hub`, `customer`, `merchant`, `store` — always sent, `customer` is a value not an omission |
|
|
| `sourceid` / `pickuplocationid` | the base or client-site id; null for a customer door |
|
|
| `pickup_source_name` | the base/site name, or the sender's name for a door pickup |
|
|
|
|
An unrecognised `pickup_source_type` should be treated as a generic pickup — new
|
|
values may be added.
|
|
|
|
`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. (`reject` accepts its reason in the body *or* as
|
|
`?reason=`, preferring the body — both spellings are honoured.)
|
|
4. `At_Customer` on `milerprofiles.availabilitystatus` means **arrived at the
|
|
pickup** — it is written only by `POST /miler/bookings/:bookingid/reached`.
|
|
The name predates the current lifecycle; a rider heading to a receiver is
|
|
`On_Delivery`.
|