9.7 KiB
Doormile backend → customer app · what changed
For: the doormile_cx app team
From: Doormile backend
Date: 15 Sep 2026
Verified against: production Postgres + Redis, and the code in this repo
Read this first
Two things decide what you can do today:
- Send the verification code as
code, nototp. That works against production right now. The server-sideotpalias described below is written but not deployed yet — do not rely on it until we confirm it has shipped. - Booking creation is currently blocked on our side, for a reason that has nothing to do with your app. See Still blocked. Sign-in will work before booking does.
Everything else here is context for why your existing integration was failing.
1. Sign-in was broken by a field name, and it was our documentation's fault
Your app posted the code as otp. The server only ever read code. So the
parsed value was always empty, the "no code supplied" branch always fired, and
every sign-in returned:
400 {"error":{"code":"invalid"},"message":"Enter the code we sent you"}
A correct code failed exactly the same way as a wrong one. No amount of SMS gateway credit would have changed it.
This was our fault, not yours. Two of our documents disagreed:
| Document | Said | Correct? |
|---|---|---|
customer-app-api-crisp.md |
otp |
❌ wrong — you built against this |
openapi-customer.yaml |
code |
✅ right |
customer-app-api-crisp.md has been corrected.
What to send
POST /api/v1/customer/auth/otp/verify
{
"identifier": "+919876543210",
"code": "1234" // ← `code`, always
}
Response 200:
{
"success": true,
"data": {
"accessToken": "eyJhbGciOi...",
"refreshToken": "d8f1e2a3...", // 64 hex chars
"expiresIn": 3600, // seconds
"customer": { "id": "cust_294", "name": "...", "phone": "+91...", "email": "" }
}
}
About the otp alias
We are adding server-side acceptance of otp as a deprecated alias, so
builds already on customers' phones start working without an app release. When
both keys are present, code wins.
It is not deployed yet. Treat it as a safety net for old installs, not as a
reason to keep sending otp. Please migrate to code.
2. Three request shapes in our docs did not match the server
Every one of these failed silently — our parser ignores unknown keys, so a
wrong field name produced a zero value, not an error. No 400, no log, just a
booking with coordinates of 0 or a missing recipient.
If you built any of these from customer-app-api-crisp.md before 11 Sep, they
need changing.
2.1 POST /customer/auth/otp/verify
| Was documented | Server actually reads |
|---|---|
otp |
code |
2.2 POST /customer/fare/estimate
| Was documented | Server actually reads |
|---|---|
pickup.latitude / pickup.longitude |
pickup.lat / pickup.lng |
pickup.stateCode / districtCode |
not read — pickup has only lat/lng |
destinations[].packages[].weightKg |
destinations[].packageCount |
Per-package weight is not an input. The estimate is priced on package count; real weight is not known until the miler weighs it at the door.
Correct request:
{
"pickup": { "lat": 13.0827, "lng": 80.2707 },
"destinations": [
{ "stateCode": "TN", "districtCode": "CHN", "packageCount": 2 },
{ "stateCode": "KA", "districtCode": "BLR", "packageCount": 1 }
]
}
2.3 POST /customer/bookings — the one with the most wrong fields
| Was documented | Server actually reads | If you send the old shape |
|---|---|---|
pickup.latitude / longitude |
pickup.lat / lng |
pickup coordinates become 0 |
pickup.contactName / contactPhone |
not read at all | silently dropped |
| destination fields flat | nested under details |
every recipient/address field dropped |
destination latitude / longitude |
details.pin.lat / lng |
drop coordinates become 0 |
estimate not documented |
is read and stored | the quote shown to the customer is not recorded |
Correct request:
{
"slotId": "slot_20260916_t2",
"pickup": {
"title": "Home",
"sub": "Flat 4B, Green Towers, Anna Nagar",
"lat": 13.0827,
"lng": 80.2707
},
"destinations": [
{
"stateCode": "TN",
"districtCode": "CHN",
"packageCount": 2,
"details": {
"street": "MG Road",
"building": "12/A",
"landmark": "Near Metro",
"recipientName": "Priya S",
"recipientPhone": "+919840123456",
"instructions": "Ring the bell",
"pin": { "lat": 13.0850, "lng": 80.2100 },
"codAmount": 450
}
}
],
"estimate": { "min": 240, "max": 310 },
"remarks": "Handle with care"
}
The booking response was also documented wrong — it returns
pickup.lat / lng, not latitude / longitude. If you parse the response
for coordinates, check that too.
3. remarks now actually saves
The top-level remarks field you were already sending was being dropped — the
server's request struct had no field for it, so BodyParser discarded it and
the booking's note was empty for every customer-app booking. The admin console
displays and searches that column, so operators saw nothing.
Fixed and merged to main. Keep sending it exactly as you are.
4. Auth flow, confirmed working end to end
We created a test customer through the live API and verified the whole sequence. There is no separate "request OTP" step after signup — signup sends the code itself.
POST /api/v1/customer/auth/signup { name, phone, email? } → 200 {sent, resendAfterSeconds}
POST /api/v1/customer/auth/otp/verify { identifier, code } → 200 {accessToken, refreshToken, ...}
For an existing account:
POST /api/v1/customer/auth/otp/request { identifier } → 200
POST /api/v1/customer/auth/otp/verify { identifier, code } → 200
Two behaviours worth coding for, both confirmed by testing:
- The code is single-use. After a successful verify it is deleted. Logging
in again requires a fresh
otp/requestfirst — re-sending the same code returns401 invalid_otp. - There is a 30-second resend cooldown. Calling
otp/requestinside that window does not issue a new code. RespectresendAfterSecondsfrom the response rather than retrying blindly.
Refresh
POST /api/v1/customer/auth/refresh { refreshToken }
Refresh tokens rotate — the presented one is revoked and replaced. Replaying an already-used refresh token revokes every session for that customer, so never keep an old one around as a fallback. Store only the newest.
Access tokens last 1 hour (expiresIn: 3600).
5. Do not offer the Email tab yet
POST /api/v1/customer/auth/otp/request {"identifier":"someone@example.com"}
→ 500 {"error":{"code":"server_error"},"message":"Something went wrong"}
SMTP is not configured on production (SMTP_HOST, SMTP_USER,
SMTP_PASSWORD are all unset). Email sign-in fails every time.
Please hide or disable the Email option until we confirm SMTP is live. Offering a path that always fails is worse than not offering it.
Still blocked on our side
You will not be able to create a booking yet, no matter what you send.
Both serviceability tables are empty on production:
serviceablestates 0 rows
serviceabledistricts 0 rows
CreateCxBooking validates every destination against that catalogue, so with
zero rows every booking is rejected with:
400 {"message":"Every destination needs a serviceable state and district"}
And GET /customer/serviceability/states returns 200 with an empty list,
so your state picker has nothing to show in the first place.
This is ours to fix — the seed data exists (seed_customer_app.sql, 5 states
including Tamil Nadu / Kerala / Karnataka / Telangana / Puducherry, 22
districts) and simply has not been applied to production. We will confirm when
it has.
Until then: sign-in and the catalogue endpoints are what you can integrate against. Booking creation will return a 400 that is not your bug.
Summary — what you need to change
| # | Change | Priority |
|---|---|---|
| 1 | Send the verification code as code, not otp |
Required — nothing works without it |
| 2 | Fare estimate: pickup.lat/lng, packageCount (no packages[].weightKg) |
Required |
| 3 | Booking: pickup.lat/lng, destination details nested under details, coords at details.pin |
Required |
| 4 | Parse the booking response's pickup.lat/lng (not latitude/longitude) |
Required |
| 5 | Send estimate: {min, max} on booking create |
Recommended — it is the dispute record |
| 6 | Hide the Email sign-in tab | Recommended |
| 7 | Handle single-use codes + the 30s resend cooldown | Recommended |
| 8 | Store only the newest refresh token, never replay an old one | Recommended |
Status of the backend changes referenced here
| Change | State |
|---|---|
remarks saved on booking create |
Merged to main |
Doc corrections (customer-app-api-crisp.md) |
In review |
otp accepted as alias for code |
In review — not deployed |
| Failed OTP send no longer burns the cooldown / rate limit | In review |
| Serviceability seed applied to production | Not done |
| SMTP configured for email OTP | Not done |
"In review" means written and tested but not yet on api.doormile.com. Build
against code and the corrected shapes — those are correct regardless of
deployment order. We will confirm when the alias and the seed are live.
Questions → the backend team.