Files
doormile_backend/docs/express-console-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

11 KiB

Doormile Express Console — API reference

The console surface only (/admin/*). 89 routes: 1 login + 88 authenticated. Everything is under https://api.doormile.com/api/v1.

Miler-app, hub-console, customer-app and CRM routes are not in this document.


Auth

POST /api/v1/admin/login
{ "email": "developer@doormile.com", "password": "admin@123" }

Returns { success, token, user: { id, name, email, role, tenantid } }. Send it on every other call as Authorization: Bearer <token>.

The token is a JWT carrying userid, email, roleid, tenantid, configid. Roles allowed on this group: 1 admin, 3 manager, 4 executive. Anything else gets 401.

Tenant scoping — read this before wiring any list screen

tenantid in the token decides what the account can see:

Token tenantid Who Sees
0 / null Doormile's own staff everything, all tenants
set (e.g. 13) a client's console login only that tenant's rows

Scoping is applied server-side. A client login cannot widen it by sending ?tenantid= or a body tenantid — on writes the server overwrites the field with the token's tenant. Build the UI as if the API returns exactly what the account is allowed to see, because it does.

Note the group is registered under a stale // all open, no token required comment in routes.go; the comment is wrong, AuthMiddleware is applied.


Dashboard, profile, reports

Method Path Notes
GET /admin/dashboard counts + today's numbers
GET /admin/reports ?from=YYYY-MM-DD&to=YYYY-MM-DD, defaults to today (IST)
GET /admin/profile current account
GET /admin/me alias of the above
PUT /admin/profile/password { "current_password": "...", "new_password": "..." } — snake_case

App users (staff logins)

Method Path
GET /admin/users
POST /admin/users
PUT /admin/users/:id
DELETE /admin/users/:id

Partners (fleet / rider suppliers)

Not the same thing as a tenant. A partner supplies vehicles and riders; a tenant is a client Doormile delivers for.

Method Path Body
GET /admin/partners
POST /admin/partners { partnername, partnertypeid, contactno, status }
GET /admin/partners/:id
PUT /admin/partners/:id
DELETE /admin/partners/:id hard delete — no soft-delete column

Tenants (client companies)

Method Path Body
GET /admin/tenants
POST /admin/tenants { tenantname, primaryemail, primarycontact, status, requiredeliveryotp }
GET /admin/tenants/:id
PUT /admin/tenants/:id requiredeliveryotp is a pointer — omit it to leave the setting alone
DELETE /admin/tenants/:id hard delete
GET /admin/tenants/:id/locations the client's sites (kitchens, branches, depots)
POST /admin/tenants/:id/locations { locationname, address, city, state, pincode, latitude, longitude, isprimary, status }
PUT /admin/tenantlocations/:id note: not nested under the tenant

requiredeliveryotp is opt-in per tenant and off by default. DailyGrubs runs without delivery OTP by decision.

Tenant customers (a client's own end customers)

Method Path Body
GET /admin/tenantcustomers
POST /admin/tenantcustomers { firstname, lastname, phone, email }
GET /admin/tenantcustomers/:id
PUT /admin/tenantcustomers/:id
DELETE /admin/tenantcustomers/:id

B2C app customers

Method Path
GET /admin/customers
PATCH /admin/customers/:id

Tenant-scoped through their bookings — a client login sees only customers who have ordered through them.

Hubs

Method Path Body
GET /admin/hubs
POST /admin/hubs { hubname, hubtype, applocationid, contactno, address, latitude, longitude, pincode, status }
GET /admin/hubs/:id
PUT /admin/hubs/:id
DELETE /admin/hubs/:id soft delete

hubtype: sorting_center | delivery_hub. applocationid is the city — Nagercoil is 5; read the rest from GET /admin/hubs rather than hardcoding.

Vehicles

Method Path Body
GET /admin/vehicles
POST /admin/vehicles { vehicleno, vehicletype, maxweight, maxvolume, partnerid, batterypercentage, status }
GET /admin/vehicles/:id
PUT /admin/vehicles/:id
DELETE /admin/vehicles/:id soft delete

Milers (riders)

Method Path Body
GET /admin/milers tenant-scoped
POST /admin/milers see below
GET /admin/milers/:id
PUT /admin/milers/:id
PUT /admin/milers/:id/block
PUT /admin/milers/:id/assign-vehicle
POST /admin/milers/:id/notify { title, message }:id is the milerprofileid
POST /admin/milers
{
  "authname": "Murali",
  "email": "murali@dailygrubs.com",
  "contactno": "9876543210",
  "password": "1234",
  "displayname": "Murali S",
  "tenantid": 13,
  "defaultvehicletype": "Bike",
  "applocationid": 1,
  "hubid": 4          // optional; without it the rider is invisible to the hub console
  // configid defaults to 1001 — the partition the miler app logs in against.
  // Do not override it. Riders created without it could never log in.
}

Bookings

Method Path Notes
GET /admin/bookings tenant-scoped list
POST /admin/expressbooking create one — passes CityGate
POST /admin/expressbooking/bulk { "bookings": [ ... ] }, max 200, per-row results
GET /admin/bookings/:id 403 if outside your tenant
POST /admin/bookings/:id/assign-miler
POST /admin/bookings/:id/assign-vehicle
PUT /admin/bookings/:id/status
POST /admin/bookings/:id/cancel
POST /admin/bookings/bulk-cancel
POST /admin/expressbooking
{
  "tenantid": 13,                  // required; forced to your own tenant on client logins
  "pickuplocationid": 15,          // a stored kitchen/branch — fills address, pincode and
                                   // coords for you, and is what makes per-site reporting work
  "customer_phone": "9876543210",  // creates a Guest customer if unknown
  "customer_name": "Ramesh",
  "deliveryaddress": "12 Cross Cut Road, Gandhipuram",
  "deliverypincode": "641012",
  "deliverycity": "Coimbatore",
  "deliverylatitude": 11.0168,
  "deliverylongitude": 76.9558,
  "service_option": "Fast",        // Normal | Fast | Superfast
  "finalprice": 120,               // the order amount the tenant pays — passed through
  "notes": "Ring the bell",
  "parcels": [
    { "itemcategory": "Food", "itemdescription": "2 meal boxes", "declaredvalue": 350 }
  ]
}

Rules worth knowing:

  • parcels must be non-empty and tenantid must exist.
  • Pickup address + pincode are required unless pickuplocationid supplies them.
  • A pickuplocationid belonging to another tenant is rejected.
  • CityGate: the pickup pincode prefix must be an open city — 641 Coimbatore, 600 Chennai, 560 Bengaluru, 500 Hyderabad, 629 Nagercoil. Any other prefix is refused at the middleware, before the handler runs.
  • Matching 3-digit pickup and delivery prefixes = hyperlocal, and the parcel goes straight to Out_for_Delivery at pickup instead of routing via a hub.
  • Auto-assignment fires after commit as a background retry loop (5 attempts, 2 min apart). The response returns before a rider is attached.

Consignments

Method Path
GET /admin/consignments
GET /admin/consignments/:id
GET /admin/consignments/track/:trackingno
PUT /admin/consignments/:id/status

Tripsheets (hub-to-hub transport)

Method Path Body
GET /admin/tripsheets
POST /admin/tripsheets { sourcehubid, destinationhubid, vehicleid, driveruserid }
GET /admin/tripsheets/:id
POST /admin/tripsheets/:id/items { consignmentid }
DELETE /admin/tripsheets/:id/items/:itemid
PUT /admin/tripsheets/:id/dispatch
PUT /admin/tripsheets/:id/arrive

Pricing

Method Path Notes
GET /admin/pricing tenant pricing rules
POST /admin/pricing { tenantid, applocationid, vehicletype, baseprice, baseweight, priceperkg, basedistance, priceperkm, handlingcharges, effectivefrom, effectiveto, currency, priority, status }
PUT /admin/pricing/:id
DELETE /admin/pricing/:id
POST /admin/pricing/simulate quote without creating anything
POST /admin/pricing/quote same handler as simulate
GET /admin/doormile-pricing Doormile's own bands
POST /admin/doormile-pricing
PUT /admin/doormile-pricing/:id
DELETE /admin/doormile-pricing/:id soft delete

Exceptions

Method Path Body
GET /admin/exceptions
POST /admin/exceptions { consignmentid, tripsheetid, hubid, exceptiontype, severity, description }
GET /admin/exceptions/:id
PUT /admin/exceptions/:id/status { resolution, status }Resolved | Closed

exceptiontype: Lost, Damaged, Misrouted, Receiver_Refused, Missing_Contents, Undeliverable. severity: Low, Medium, High, Critical.

Competitive intel

Method Path
GET/POST /admin/competitor-branches
PUT/DELETE /admin/competitor-branches/:id
GET/POST /admin/carrier-pricing
PUT/DELETE /admin/carrier-pricing/:id

Conventions across every endpoint

  • Envelope: { "success": true, "data": ... } on success, { "success": false, "message": "..." } on failure. Lists add total, paginated lists add page.
  • Pagination: ?pageno=1&pagesize=100. Default 500, cap 1000.
  • Rate limits: 300/min per IP globally, 10/min shared across all credential endpoints. Behind the ingress this keys on the proxy IP unless TRUSTED_PROXIES is set.
  • Timestamps are IST (Asia/Kolkata) wall-clock in timestamp without time zone columns. Send dates as YYYY-MM-DD, not epochs.
  • Soft delete exists on Hub, Vehicle, Consignment, Tripsheet, TripsheetItem, ConsignmentException, DoormilePricing, CarrierPricing. Partner and Tenant are hard-deleted.

Not exercised yet

Roughly half these routes have never had a real request against them. Exercised end-to-end so far: login, dashboard, reports, tenants + locations, milers (create/list/notify), expressbooking (single), bookings list/detail, assign-miler, consignments, profile password. Treat the rest as written but unproven.