8.6 KiB
Doormile — order to delivery
Every endpoint in the DailyGrubs path, the payload that goes in, and what
actually changes when it does. Base URL https://api.doormile.com/api/v1.
Payload shapes are taken from the request structs in this repo, not from documentation — where the two disagree, the code is right. Where a field is optional it says so.
Auth
Two separate logins.
Console. The token carries tenantid, which is what scopes a client to
their own data. A client passing another tenant's ?tenantid= gets 403; reading
another tenant's resource by id gets 404, so ids aren't probeable.
POST /admin/login
{ "email": "info@dailygrubs.com", "password": "admin" }
→ { "token": "eyJ…", "user": { "id": 43, "tenantid": 13 } }
Rider. Two calls, and configid must be 1001 on both — a Doormile login
partition with no jupiter equivalent. Omitting it is the most common reason a
rider looks like they don't exist.
POST /miler/login
{ "phone": "9787698259", "configid": 1001 }
POST /miler/verify-pin
{ "phone": "9787698259", "pin": "1234", "configid": 1001, "device_token": "fcm-…" }
→ { "token": "eyJ…" }
POST /miler/reset-pin is admin-only despite sitting under /miler. A phone
number is the login identifier, not a secret — left open, reset-then-verify takes
over any rider account in two calls. The rider app must not call it.
1. Create a booking
POST /admin/expressbooking
{
"tenantid": 13,
"tenantlocationid": 20, // the kitchen. optional — inferred if omitted
"customer_phone": "9876500011",
"customer_name": "Priya R",
"pickupaddress": "DailyGrubs RS Puram Kitchen",
"pickuppincode": "641002",
"pickuplatitude": 11.004500, "pickuplongitude": 76.961200,
"deliveryaddress": "12 Bharathi Rd, Peelamedu",
"deliverypincode": "641004",
"deliverylatitude": 11.051000, "deliverylongitude": 76.930000,
"service_option": "Fast",
"finalprice": 65,
"parcels": [ { "itemcategory": "Food", "weight": 1.2 } ]
}
→ { "bookingid": 118, "bookingno": "DM-BK-…", "status": "Created" }
Writes a pickupbookings row plus its parcels and price.
On tenantlocationid. This is the client's own site — the kitchen. Omit it
and it's resolved from the pickup coordinates: nearest stored site within 150m,
falling back to an address match, nil when unsure. It is what per-kitchen
reporting groups by.
pickuplocationid is accepted only as a legacy alias and is never stored as
given — the column of that name foreign-keys to appcustomerlocations (a B2C
customer's saved address), so writing a client site id into it fails the insert.
CityGate. The pickup pincode prefix must be an open city: 641 Coimbatore,
600 Chennai, 560 Bengaluru, 500 Hyderabad, 629 Nagercoil. Anything else
is refused at creation. jupiter had no such gate.
2. Or create them in bulk
POST /admin/expressbooking/bulk
{ "bookings": [ { …same shape… }, { … } ] } // max 200
→ { "results": [
{ "index": 0, "success": true, "bookingid": 119, "bookingno": "DM-BK-…" },
{ "index": 1, "success": false, "error": "pickup pincode not serviceable" }
] }
Per-row results, never all-or-nothing — one bad address doesn't lose the other
199. Each booking is its own transaction. A client login cannot use bulk to
smuggle in another tenant's id; tenantid is pinned to the caller's own.
3. A rider is found — automatic
No call needed. Creation publishes booking.assignment_requested to JetStream
after the transaction commits. A worker searches riders within 10km via Redis
GEO, scores them through the AI layer, and commits the assignment. If nobody is
available it retries 5 times, 2 minutes apart, then publishes
booking.assignment_failed for the dispatch agent.
The retry state lives in NATS, not in process memory, so a pod restart no longer loses a booking mid-wait.
To assign by hand instead:
POST /admin/bookings/:id/assign-miler
{ "mileruserid": 38 }
4. Clear a hub queue, and order the stops
POST /hub/bookings/batch-assign
{ "bookingids": [118, 119, 120], "max_per_rider": 5 }
→ { "assigned": 3, "skipped": 0, "riderssequenced": 1,
"results": [ { "bookingid": 118, "assigned": true,
"mileruserid": 38, "distance_km": 1.4 } ] }
This is the only place stops get ordered. After assigning, each affected
rider's whole active set is sent to the Route Optimization API
(POST /api/v1/optimization/doormile/sequence on routes.workolik.com), which
returns a road-network sequence via Valhalla — not straight-line distance. The
step, per-leg distance, cumulative distance and ETA are written onto
bookingassignments.
Assignment picks who; sequencing picks what order. They are separate,
and sequencing can only run once a rider is known — which is why bulk creation
cannot sequence anything, however many bookings you send. jupiter got away with
sequencing inside createdeliveries because that payload was already one
rider's run; Doormile's bulk endpoint is up to 200 bookings across many riders.
Sequencing is best-effort and runs after the assignments commit: the optimizer is a separate service over the network, and it being down must leave bookings assigned but unordered, never undo the batch.
5. The rider runs the route
GET /miler/assignments
→ [ { "bookingassignmentid": 555, "bookingid": 120,
"step": 1, "previouskms": 4.0, "cumulativekms": 4.0,
"etaminutes": 14, "cumulativeeta": 14 }, … ]
Returned in step order. step: 0 means not sequenced — never first — and
sorts to the end. A rider with one stop is never sequenced, so 0 is common.
Then, per booking:
POST /miler/assignments/:id/accept
POST /miler/bookings/:bookingid/reached
POST /miler/bookings/:bookingid/parcel
{ "parcels": [ { "weight": 1.4, "length": 20, "width": 15, "height": 10 } ] }
POST /miler/bookings/:bookingid/payment
{ "amount": 65, "paymentmode": "Cash", "transactionref": "" }
POST /miler/bookings/:bookingid/pickup-complete
pickup-complete is the pivot the old system had no concept of. It converts the
booking into a consignment, recomputes chargeable weight from the dimensions
the rider actually measured, carries the kitchen across, and decides routing —
matching 3-digit pincode prefixes go straight to Out_for_Delivery (hyperlocal),
everything else routes via a hub.
Other rider actions: vehicle-required (needs a bigger vehicle), cancel
(before pickup).
6. Deliver
POST /miler/consignments/:id/deliver
{ "deliveredtoname": "Priya R",
"photourl": "https://…", "receiversignatureurl": "https://…",
"lat": 11.051, "lon": 76.93,
"otp": "418317" } // only when the tenant requires it
Marks the consignment delivered and writes a history row. Delivery OTP is
opt-in per tenant (Tenant.Requiredeliveryotp, default off — off for
DailyGrubs). When on it is verified server-side and never serialised outward:
returning it would hand the rider the code they are meant to be told.
Couldn't deliver? POST /miler/consignments/:id/skip increments attemptcount
rather than failing the parcel.
Watching it
| What | Endpoint |
|---|---|
| One booking end to end | GET /admin/bookings/:id/track |
| A parcel's GPS trail + history | GET /admin/consignments/:id/logs |
| Riders today | GET /admin/milers/summary?from=&to= |
| One rider's logs | GET /admin/milers/:id/logs |
| Per kitchen | GET /admin/locations/summary?tenantid=&locationid= |
| Reports | GET /admin/reports?from=&to=&tenantid=&locationid=&hubid= |
Admin miler endpoints key on milerprofileid, while assign-miler takes a
mileruserid in the body — different identity spaces on adjacent endpoints.
Worth checking which one you have.
State
| Capability | State |
|---|---|
| Booking create, bulk, tracking, reports | deployed |
| Tenant scoping for client logins | deployed |
| Per-kitchen attribution | deployed |
| Durable assignment retry (JetStream) | built, not deployed |
| Stop sequencing (Doormile side) | built, not deployed |
/optimization/doormile/sequence (routes.workolik.com) |
built, not deployed |
| Multi-stop optimizer service itself | live |
Sequencing needs two deploys, not one — the endpoint in the route-optimizer
service (docker-compose on 31.97.228.132, behind Traefik) and Doormile's
client that calls it. Ship one without the other and sequencing fails quietly:
bookings stay assigned but unordered, and riders choose their own order.
Nothing on the client side has moved. The rider app and express console still
call jupiter.nearle.app. These endpoints exist and are tested; no production
traffic uses them yet.