Adds the five rider/tracking endpoints, the ?tenantid= staff filter and the 403-vs-404 refusal rules, and replaces the guesswork coverage note with what was actually run against production on 2026-08-06. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
15 KiB
Doormile Express Console — API reference
The console surface only (/admin/*). 94 routes: 1 login + 93 authenticated.
Everything is under https://api.doormile.com/api/v1.
Verified against the build deployed 2026-08-06 12:41 IST.
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. Build the UI as if the API returns exactly what the account is allowed to see, because it does.
?tenantid= — the staff-only filter. Doormile staff pass it to narrow any
of the list/summary endpoints to one client:
GET /admin/milers?tenantid=13 → DailyGrubs' 5 riders
GET /admin/dashboard?tenantid=13 → DailyGrubs' counters
GET /admin/reports?tenantid=13&from=…&to=…
Supported on dashboard, reports, milers, milers/summary, customers,
bookings, consignments. Omit it for the whole network.
A client login passing another tenant's id gets 403 — not their own data silently relabelled. Passing their own id, or omitting it, works normally.
Reading one resource by id that belongs to another tenant returns 404, not
403, so ids outside your own fleet aren't probeable. This covers bookings,
consignments and riders, on reads and writes — POST /admin/bookings/:id/ assign-miler on someone else's booking is refused before the write, not after.
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 / notes |
|---|---|---|
| GET | /admin/milers |
tenant-scoped; ?applocationid=&hubid=&tenantid= |
| GET | /admin/milers/summary |
the roster table — see below |
| POST | /admin/milers |
see below |
| GET | /admin/milers/:id |
404 outside your fleet |
| GET | /admin/milers/:id/logs |
GPS trail — see below |
| GET | /admin/milers/:id/activity |
one rider's detail — see below |
| PUT | /admin/milers/:id |
|
| PUT | /admin/milers/:id/block |
|
| PUT | /admin/milers/:id/assign-vehicle |
{ vehicleid } |
| POST | /admin/milers/:id/notify |
{ title, message } — :id is the milerprofileid |
Every :id here is the milerprofileid, not the userid.
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.
}
The rider screens
These are the express-console equivalents of jupiter's getridersummary and
its rider/delivery logs.
GET /admin/milers/summary?from=&to=&applocationid=&hubid=&tenantid=
One row per rider — live state on the left, range totals on the right. Defaults to today. This is the rider list screen.
{ "milerprofileid": 78, "userid": 41, "displayname": "Murali P",
"phone": "…", "availabilitystatus": "Offline", "defaultvehicletype": "Bike",
"hubid": null, "hubname": "", "rating": 5,
"onduty": false, "dutystartedat": null,
"currentlatitude": 11.0163, "currentlongitude": 77.0147,
"lastlocationupdatedat": "…", "lastpingat": "2026-08-05T18:10:00Z",
"assigned": 0, "accepted": 0, "rejected": 0,
"completed": 1, "cancelled": 0, "delivered": 1,
"riderkms": 0, "ridercharges": 0 }
lastpingat comes from Redis telemetry, lastlocationupdatedat from the
profile — they differ, and the first is the better staleness signal.
GET /admin/milers/:id/logs?from=&to=&limit=
The GPS trail. limit defaults to 500, caps at 5000. Returns
{ data: [MilerLog…], total, distancekm, miler: {…}, from, to } where
distancekm is the haversine sum over consecutive points. Telemetry coords are
strings, since that's what the rider app sends.
GET /admin/milers/:id/activity?from=&to=
One rider's detail page: assignments[], the bookings[] behind them,
dutylogs[], breaklogs[], lastpingat, and totals: { assignments, delivered, riderkms, ridercharges, dutyminutes }.
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 |
404 if outside your tenant |
| GET | /admin/bookings/:id/track |
the tracking screen — see below |
| 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:
parcelsmust be non-empty andtenantidmust exist.- Pickup address + pincode are required unless
pickuplocationidsupplies them. - A
pickuplocationidbelonging to another tenant is rejected. - CityGate: the pickup pincode prefix must be an open city —
641Coimbatore,600Chennai,560Bengaluru,500Hyderabad,629Nagercoil. 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_Deliveryat 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.
Tracking one booking end to end
GET /admin/bookings/:id/track — one call for the whole lifecycle, so the
tracking screen doesn't have to stitch five requests together:
{
"booking": { …with parcels, serviceoptions, payments… },
"assignments": [ { mileruserid, assignmentstatus, assignedat, acceptedat,
completedat, riderkms, ridercharges } ], // every attempt
"riders": [ { userid, displayname, phone, … } ], // one per attempt
"livelocation": { "latitude": …, "longitude": … }, // null if no ping in 30 min
"lastpingat": "…",
"consignment": { … }, // present once picked up
"history": [ { eventstatus, remarks, createdat } ],
"telemetry": [ ConsignmentLog… ],
"deliveryproof": { deliveredtoname, photourl, geolatitude, … }
}
assignments holds every attempt, not just the current one — a rejected
first assignment is exactly what ops needs when asking why a pickup was slow.
Consignments
| Method | Path | Notes |
|---|---|---|
| GET | /admin/consignments |
?tenantid= for staff |
| GET | /admin/consignments/:id |
|
| GET | /admin/consignments/:id/logs |
event history + telemetry + proof |
| GET | /admin/consignments/track/:trackingno |
|
| PUT | /admin/consignments/:id/status |
/:id/logs returns { consignment, history[], telemetry[], deliveryproof? } —
the durable Postgres event log and the rider's Redis trail in one response,
rather than making the console reconcile two stores.
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 addtotal, paginated lists addpage. - 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_PROXIESis set. - Timestamps are IST (
Asia/Kolkata) wall-clock intimestamp without time zonecolumns. Send dates asYYYY-MM-DD, not epochs. - Soft delete exists on Hub, Vehicle, Consignment, Tripsheet, TripsheetItem, ConsignmentException, DoormilePricing, CarrierPricing. Partner and Tenant are hard-deleted.
Verified against production, 2026-08-06
Run as both a client login (info@dailygrubs.com, tenant 13) and Doormile
staff (developer@doormile.com, tenant 0):
- dashboard, reports, milers, milers/summary, milers/:id, milers/:id/logs, milers/:id/activity, customers, bookings, bookings/:id, bookings/:id/track, consignments, consignments/:id/logs, tenants, tenants/:id/locations
?tenantid=narrowing on all of the above, for staff- 403 on a client requesting another tenant; 404 on cross-tenant reads and
on
assign-miler/status/cancelwrites, with the target row confirmed unmodified afterwards
Still unproven: tripsheets, exceptions, vehicles, competitor-branches,
carrier-pricing, doormile-pricing, app-users CRUD, partner CRUD, bulk booking
create/cancel, assign-vehicle, block. Written, compiled, never called with
a real request.