20 KiB
Doormile Hub Console — Frontend Integration Guide
Base URL: https://api.doormile.com
Auth: Bearer token in Authorization header on every authenticated request
Content-Type: application/json on all POST / PATCH calls
How the flow works
- Staff opens the console → Login page
POST /api/v1/hub/login→ receives JWT + hub context- Store token + hub context in localStorage
- Every page reads hub name/city from stored context (no extra call)
- Every API call sends
Authorization: Bearer <token> - On 401 → clear localStorage → redirect to
/login
1. Authentication
POST /api/v1/hub/login
No auth header needed.
Request body:
{
"email": "hub.coimbatore@doormile.in",
"password": "password123"
}
Response:
{
"success": true,
"token": "eyJhbGci...",
"hub": {
"hubid": 1,
"hubname": "Coimbatore Jupiter Hub",
"hubtype": "sorting_center",
"city": "Coimbatore",
"capacity": 50
},
"staff": {
"displayname": "Coimbatore Hub Staff",
"email": "hub.coimbatore@doormile.in",
"role": 6
},
"is_doormile_staff": true
}
What to store in localStorage after login:
localStorage.setItem('hub_token', data.token)
localStorage.setItem('hub_context', JSON.stringify(data.hub))
localStorage.setItem('hub_staff', JSON.stringify(data.staff))
localStorage.setItem('hub_is_doormile', String(data.is_doormile_staff))
Test accounts:
| Password | Hub | Access | |
|---|---|---|---|
| hub.coimbatore@doormile.in | password123 | Coimbatore Jupiter Hub | Full (Doormile staff) |
| hub.hyderabad@doormile.in | password123 | Hyderabad hub | Full (Doormile staff) |
| hub.bangalore@doormile.in | password123 | Bangalore hub | Full (Doormile staff) |
| hub.chennai@doormile.in | password123 | Chennai hub | Full (Doormile staff) |
| hub@kpmtravels.in | password123 | Hub 1 | Restricted (partner — no hub management) |
2. Dashboard
GET /api/v1/hub/dashboard
Returns live KPI stats for the logged-in staff's hub.
Response:
{
"success": true,
"data": {
"parcels_received_today": 142,
"milers_available": 6,
"milers_on_duty": 12,
"pending_pickups": 8,
"batches_sent_today": 3,
"exceptions": 2
}
}
Which UI element uses which field:
| Dashboard card | Field |
|---|---|
| Total Parcels Received | parcels_received_today |
| Available Milers | milers_available |
| Milers on Duty | milers_on_duty |
| Pending Pickups | pending_pickups |
| Batches Sent Today | batches_sent_today |
| Needs Checking | exceptions |
Note: Hub name shown in the header/title comes from localStorage (hub_context.hubname) — not from this endpoint. No extra call needed.
3. Inbound — Receive Parcels
GET /api/v1/hub/inbound/today
Returns all parcels inbounded at this hub today. Now returns sendername, originname, destinationname, temperature (readable names instead of raw IDs and pincodes).
Response:
{
"success": true,
"data": [
{
"bookingid": 15,
"trackingnumber": "DM-882201",
"sendername": "Acme Corp",
"originname": "Mumbai Hub",
"destinationname": "Dwarka Sec 12, Coimbatore",
"weight": "2.4 kg",
"condition": "Good",
"temperature": "N/A",
"shelf": "Zone A (Shelf 1)",
"inboundedat": "2026-07-04T09:15:00Z"
}
],
"total": 1
}
POST /api/v1/hub/bookings/:id/inbound
Scan a parcel in. :id is the booking/consignment ID.
Request body:
{
"tracking_id": "DM-882201",
"condition": "Good",
"temperature": "N/A",
"shelf": "Zone A (Shelf 1)",
"weight": "2.4 kg"
}
Condition values: Good · Damaged Box · Wet / Crushed · Missing Label
Temperature: Free text — "4.1°C" for cold chain items, "N/A" for dry goods
Response:
{
"success": true,
"data": {
"bookingid": 15,
"trackingnumber": "DM-882201",
"recommended_shelf": "Zone A (Shelf 1)",
"status": "At_Hub"
}
}
Shelf recommendation logic (backend handles this automatically):
- Condition contains Damaged / Wet / Missing → Exception Area
- Temperature is not N/A → Zone C (Cold Room)
- Otherwise → Zone A or Zone B based on destination
Error — tracking ID not found:
{
"success": false,
"message": "booking not found"
}
HTTP 404.
4. Order Assignment — Pickup Requests
GET /api/v1/hub/bookings/unassigned
Returns all pending pickup requests for this hub's city.
Response:
{
"success": true,
"data": [
{
"bookingid": 21,
"customerName": "Ramesh K.",
"pickupaddress": "Gandhipuram, Coimbatore",
"deliveryaddress": "Hitech City, Hyderabad",
"packagedescription": "Small Box, 2kg",
"createdat": "2026-07-04T08:45:00Z",
"status": "Pending_Pickup"
}
],
"total": 3
}
For assigning a miler to these bookings, see Section 14 — Hub Assignment Override.
5. Dispatch & Batches
GET /api/v1/hub/batches
Returns all outgoing batches for this hub.
Response:
{
"success": true,
"data": [
{
"tripsheetid": 1,
"batchlabel": "BATCH-9281",
"batchkind": "transfer",
"destinationlabel": "Mumbai Hub (BOM-02)",
"vehicle": "Truck DL-01-BZ-8055",
"itemcount": 154,
"status": "Draft",
"createdat": "2026-07-04T08:00:00Z",
"dispatchtime": null
}
],
"total": 1
}
Status values: Draft → Ready → Dispatched
Batchkind values: local · transfer
POST /api/v1/hub/batches
Create a new outgoing batch.
Request body:
{
"route": "Transfer to Mumbai Hub",
"destination": "Mumbai Hub (BOM-02)",
"vehicle": "Truck DL-01-BZ-8055",
"parcels_count": 154,
"kind": "transfer"
}
Response:
{
"success": true,
"data": {
"tripsheetid": 2,
"batchlabel": "BATCH-9282",
"status": "Draft"
}
}
PATCH /api/v1/hub/batches/:id/status
Move a batch through its status flow.
Request body — mark as ready:
{ "status": "Ready" }
Request body — dispatch (send out):
{ "status": "Dispatched" }
Response:
{
"success": true,
"data": {
"tripsheetid": 1,
"status": "Ready",
"dispatchtime": null
}
}
Invalid status transition → 400:
{
"success": false,
"message": "invalid status"
}
6. Milers
GET /api/v1/hub/milers
Returns all milers assigned to this hub, now with operational stats per miler.
Response:
{
"success": true,
"data": [
{
"userid": 4,
"displayname": "Rajesh Kumar",
"phone": "+91 98765 43210",
"vehicleid": 1,
"hubid": 1,
"availabilitystatus": "Available",
"rating": 4.8,
"completedorders": 45,
"cancelledorders": 2,
"currentlat": 11.0168,
"currentlon": 76.9558,
"device_token": "fcm_token_here",
"zones": ["641001", "641012"],
"assignedload": 2,
"capacity": 30,
"pickupspending": 1,
"codcollected": 4500.00,
"codpending": 1200.00,
"checkinat": "2026-07-04T08:12:00Z",
"hoursactive": 6.4,
"isverified": true
}
],
"total": 6
}
Availability status values: Available · Assigned · On_Break · Offline
The zones, assignedload, capacity, pickupspending, codcollected, codpending, checkinat, hoursactive, isverified fields were the empty fields the Milers page was trying to show. Now populated from real data.
POST /api/v1/admin/milers
Create (onboard) a new miler. Hub JWT is accepted here.
Request body:
{
"displayname": "Muthu Kumar",
"phone": "+91 90011 22334",
"hubid": 1,
"vehicleid": 2,
"availabilitystatus": "Available"
}
Response:
{
"success": true,
"data": {
"userid": 10,
"displayname": "Muthu Kumar"
}
}
PATCH /api/v1/admin/milers/:id
Update a miler's details or status.
Request body (any subset of fields):
{
"availabilitystatus": "On_Break",
"hubid": 2
}
DELETE /api/v1/admin/milers/:id
Remove a miler from the roster.
Response:
{
"success": true,
"message": "miler removed"
}
7. Routing — Where Does It Go?
GET /api/v1/hub/routing/:trackingno
Scan a parcel and get its sort destination. :trackingno is the tracking number scanned at the sort station.
Response:
{
"success": true,
"data": {
"trackingno": "DM-TRK-0015",
"destination": "Peelamedu, 641004",
"recommendedshelf": "Zone B",
"nexthop": "Local delivery",
"condition": "Good",
"iscoldchain": false,
"weight": "2.4 kg",
"customername": "Ramesh K.",
"bookingid": 15
}
}
nexthop values: Local delivery · Transfer to Mumbai Hub (or whichever destination hub).
Use recommendedshelf to show the big shelf indicator UI.
Error — tracking number not found → 404. Show a "Parcel not found" error state.
{
"success": false,
"message": "parcel not found"
}
8. Rider Routes
GET /api/v1/hub/rider-routes
Returns all milers at this hub with their planned stops today.
GET /api/v1/hub/milers/:id/route
Returns the planned stops for a single miler. :id is the miler user ID.
Response (same data[] shape for both; the single-miler endpoint returns one entry):
{
"success": true,
"data": [
{
"mileruserid": 4,
"milername": "Rajesh Kumar",
"mode": "pickup",
"totalstops": 5,
"completedstops": 2,
"totaldistance_km": 12.4,
"stops": [
{ "seq": 1, "address": "Gandhipuram, Coimbatore", "lat": 11.0168, "lon": 76.9558, "items": 1, "bookingid": 20, "status": "completed", "eta_minutes": 0 },
{ "seq": 2, "address": "RS Puram, Coimbatore", "lat": 11.001, "lon": 76.951, "items": 2, "bookingid": 21, "status": "pending", "eta_minutes": 12 }
]
}
]
}
mode values: pickup · delivery
Stop status values: pending · in_progress · completed
Pass stops[].lat/lon to OSRM to draw the route line — the OSRM call stays client-side.
9. Live Trucks
GET /api/v1/hub/tripsheets/in-transit
Returns transfer trucks currently in transit, with interpolated live positions. Poll this every 5 seconds alongside miler locations.
Response:
{
"success": true,
"data": [
{
"tripsheetid": 3,
"tripsheetno": "DM-TS-0003",
"label": "Chennai Jupiter Hub → Coimbatore Jupiter Hub",
"originlat": 13.0827, "originlon": 80.2707,
"destlat": 11.0168, "destlon": 76.9558,
"currentlat": 12.1, "currentlon": 78.9,
"status": "In_Transit",
"progresspct": 40,
"vehicleno": "TN-04-AX-8822",
"itemcount": 154,
"eta_minutes": 180
}
]
}
currentlat/lon gives the truck's position on the map. Replace the fake animated truck with these real coordinates.
10. Arriving Vehicles
GET /api/v1/hub/inbound/vehicles
Returns trucks arriving at this hub (Dashboard → "Trucks Arriving" table).
Response:
{
"success": true,
"data": [
{
"vehicleno": "TN-04-AX-8822",
"origin": "Chennai Jupiter Hub",
"tripsheetid": 3,
"status": "On the way",
"eta": "2 hrs 30 min",
"unloadedpct": 0,
"itemcount": 154
}
]
}
Status values: On the way · Unloading
(Dispatched maps to On the way, Arrived maps to Unloading.)
11. Activity Feed
GET /api/v1/hub/activity?limit=10
Returns a real event feed for this hub (UNION across 4 sources: inbound, dispatch, exceptions, sorting).
Response:
{
"success": true,
"data": [
{ "time": "2026-07-04T11:24:00Z", "type": "exception", "text": "Exception raised: Damaged on DM-TRK-999999" },
{ "time": "2026-07-04T11:22:00Z", "type": "inbound", "text": "Received parcel DM-TRK-999999 from Coimbatore Jupiter Hub" },
{ "time": "2026-07-04T11:20:00Z", "type": "dispatch", "text": "Dispatched batch DM-TS-0003 to Mumbai Hub" }
],
"total": 3
}
Type values: inbound · dispatch · exception · sorting
Use type to pick the icon color — same color scheme as the existing mock.
12. Zones
GET /api/v1/hub/zones
Returns the delivery zone breakdown for this hub (Dashboard → "Delivery Areas").
Response:
{
"success": true,
"data": [
{ "zone": "641001", "zonename": "Gandhipuram", "parcels": 12, "milers": 2, "status": "Active" },
{ "zone": "641004", "zonename": "Peelamedu", "parcels": 8, "milers": 1, "status": "Active" },
{ "zone": "641012", "zonename": "Jupiter Nagar","parcels": 3, "milers": 0, "status": "Need Milers" }
]
}
Status values: Active · Need Milers — use this to color the status chip.
13. Notifications
GET /api/v1/hub/notifications
Returns notifications for the header bell icon, generated from real events.
Response:
{
"success": true,
"data": [
{ "id": 1, "title": "Exception: Damaged", "type": "exception", "time": "3 min ago", "read": false },
{ "id": 2, "title": "Truck arriving from Chennai Hub", "type": "inbound", "time": "5 min ago", "read": false },
{ "id": 3, "title": "Pickup waiting 30+ min", "type": "alert", "time": "12 min ago", "read": false }
],
"total": 3
}
Type values: exception · inbound · warning · alert
PATCH /api/v1/hub/notifications/:id/read
Mark a notification as read.
Response:
{ "success": true }
On bell click → GET /hub/notifications. On item click → PATCH /hub/notifications/:id/read, then mark it read in local state.
14. Hub Assignment Override
The old admin assign endpoint (POST /admin/bookings/:id/assign-miler) returned 403 for hub staff. Two new hub-scoped endpoints replace it.
POST /api/v1/hub/bookings/:id/assign-miler
Manual assign — hub staff picks the miler.
Request body:
{ "mileruserid": 4 }
Response:
{
"success": true,
"data": {
"bookingid": 21,
"mileruserid": 4,
"milername": "Rajesh Kumar",
"status": "Miler_Assigned"
}
}
Use this on the Pickup Requests page when staff manually selects a miler from the dialog.
POST /api/v1/hub/bookings/:id/auto-assign
Auto-assign — AI engine picks the miler.
Request body:
{}
Response (success):
{
"success": true,
"data": {
"mileruserid": 4,
"milername": "Rajesh Kumar",
"distance_km": 0.4
}
}
Response (no miler) — HTTP 422:
{
"success": false,
"message": "No eligible miler found in range"
}
Response (timeout) — HTTP 202:
{
"success": true,
"message": "assignment in progress"
}
Use this for the Auto-Assign bulk button. Loop over selected booking IDs and call this for each one.
Backend implementation notes: shared AssignMilerToBooking() service (booking_assignment_service.go), TryAssignOnce() single-attempt wrapper on crm_assignment, assignedbyuserid is nullable for hub-initiated assignments, and AdminAssignMiler now sends FCM to the miler.
15. Hub Management (Doormile staff only)
These endpoints return 403 for partner accounts (is_doormile_staff = false).
Use the is_doormile_staff flag from localStorage to show/hide the UI for these.
GET /api/v1/hub/hubs
List all hubs in the logged-in staff's city. Now includes lat/lon for map pins.
Response:
{
"success": true,
"data": [
{
"hubid": 1,
"hubname": "Coimbatore Jupiter Hub",
"hubtype": "sorting_center",
"capacity": 50,
"status": "active",
"has_staff": true,
"lat": 11.0168,
"lon": 76.9558
},
{
"hubid": 2,
"hubname": "Coimbatore Spoke 1",
"hubtype": "spoke",
"capacity": 50,
"status": "active",
"has_staff": false,
"lat": 11.0210,
"lon": 76.9610
}
]
}
Use lat/lon to place hub pins on the Live Map.
POST /api/v1/hub/hubs
Create a new hub in the staff's city.
Request body:
{
"hubname": "Coimbatore RS Puram Hub",
"hubtype": "spoke",
"capacity": 30,
"contact": "+91 98765 00000",
"address": "RS Puram, Coimbatore",
"pincode": "641002"
}
Note: applocationid (city) is set automatically from the logged-in staff's hub — cannot be overridden.
POST /api/v1/hub/staff
Create a hub staff login for any hub in the city.
Request body:
{
"hubid": 2,
"email": "hub.rspuram@doormile.in",
"password": "securepassword",
"displayname": "RS Puram Hub Staff",
"tenantid": null
}
Set tenantid to the partner's tenant ID for partner accounts, null for Doormile staff.
Response:
{
"success": true,
"data": {
"staffid": 6,
"email": "hub.rspuram@doormile.in",
"hubid": 2
}
}
Error response shape
All errors follow the same shape:
{
"success": false,
"message": "human readable error description"
}
| HTTP code | Meaning |
|---|---|
| 400 | Bad request — invalid body or status transition |
| 401 | Token expired or missing → redirect to /login |
| 202 | Accepted — async assignment in progress |
| 403 | Forbidden — partner account trying Doormile-only endpoint |
| 404 | Resource not found (booking ID, miler ID, tracking no etc.) |
| 422 | Unprocessable — no eligible miler found in range |
| 500 | Server error — show generic "something went wrong" toast |
Page-to-endpoint map (quick reference)
| Page | Endpoints called |
|---|---|
| Login | POST /hub/login |
| Dashboard | GET /hub/dashboard · GET /hub/inbound/vehicles · GET /hub/activity · GET /hub/zones |
| Receive Parcels | GET /hub/inbound/today · POST /hub/bookings/:id/inbound |
| Pickup Requests | GET /hub/bookings/unassigned · GET /hub/milers · POST /hub/bookings/:id/assign-miler · POST /hub/bookings/:id/auto-assign |
| Where Does It Go? (Routing) | GET /hub/routing/:trackingno |
| Dispatch | GET /hub/batches · POST /hub/batches · PATCH /hub/batches/:id/status |
| Milers | GET /hub/milers · POST /admin/milers · PATCH /admin/milers/:id · DELETE /admin/milers/:id |
| Rider Routes | GET /hub/rider-routes · GET /hub/milers/:id/route |
| Live Map | GET /admin/milers/locations (poll 5s) · GET /hub/tripsheets/in-transit (poll 5s) · GET /hub/hubs |
| Header — Notifications | GET /hub/notifications · PATCH /hub/notifications/:id/read |
| Hub Settings | GET /hub/hubs · POST /hub/hubs · POST /hub/staff |
Frontend implementation notes
Token storage:
// On login success:
localStorage.setItem('hub_token', response.token)
localStorage.setItem('hub_context', JSON.stringify(response.hub))
localStorage.setItem('hub_staff', JSON.stringify(response.staff))
localStorage.setItem('hub_is_doormile', String(response.is_doormile_staff))
// On every API call:
headers: { 'Authorization': `Bearer ${localStorage.getItem('hub_token')}` }
// On logout or 401:
localStorage.clear()
navigate('/login')
Show hub name dynamically (not hardcoded):
const hub = JSON.parse(localStorage.getItem('hub_context'))
// hub.hubname → "Coimbatore Jupiter Hub"
// hub.city → "Coimbatore"
// hub.hubid → 1
Hide hub management UI for partners:
const isDoormile = localStorage.getItem('hub_is_doormile') === 'true'
// Show "Add New Hub" / "Add Staff" buttons only if isDoormile === true
// KPM partner account gets false → those buttons hidden
On 401 — token expired, force re-login:
if (response.status === 401) {
localStorage.clear()
window.location.href = '/login'
}
Polling intervals:
- Miler locations → every 5 seconds
- Truck positions → every 5 seconds (same call cycle)
- Dashboard stats → every 30 seconds
- Notifications → every 60 seconds
All endpoints live at https://api.doormile.com · Hub Console Backend v1.0 complete