Files
doormile_backend/docs/customer-app-handover-2026-09-15.md

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:

  1. Send the verification code as code, not otp. That works against production right now. The server-side otp alias described below is written but not deployed yet — do not rely on it until we confirm it has shipped.
  2. 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/request first — re-sending the same code returns 401 invalid_otp.
  • There is a 30-second resend cooldown. Calling otp/request inside that window does not issue a new code. Respect resendAfterSeconds from 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.