Mobile Schools — Integration Guide
For mobile development. Everything needed to power the Education step school picker — state + district dropdowns and the school-name typeahead. No backend changes required.
- API base URL (local):
http://localhost:3001 - Auth: either an
onboardingToken(during signup) OR anaccessToken(after login) — both work on every endpoint below. Send asAuthorization: Bearer <token>. - Send
X-Client-Type: mobileon every request.
Short version — just the calls
GET /api/schools/states → list of states
GET /api/schools/districts?state=Maharashtra → districts for that state
GET /api/schools?q=delhi&state=&district=&pinCode=&limit=20
→ school typeahead
GET /api/schools/:id → re-hydrate a previously picked school
Pick any combination — user can drill down state → district → school,
or skip both upper dropdowns and search the school name directly.
Either path returns the same school payload (with state + district
embedded), so the upper dropdowns can be back-filled from the selection.
1 — States dropdown
1.1 — Endpoint
GET /api/schools/states
Authorization: Bearer <onboardingToken | accessToken>
Rate limit: 30 requests / 60 seconds per user.
Cache: Cache-Control: public, max-age=3600 — cache aggressively client-side.
Response:
{
"success": true,
"data": [
"Andaman & Nicobar Islands",
"Andhra Pradesh",
"Arunachal Pradesh",
"Assam",
"Bihar",
"...",
],
}
Strings are case-preserved as they appear in the catalog. Pass them back
verbatim to /api/schools and /api/schools/districts (server still
accepts case-insensitively, but verbatim matches the typeahead labels).
2 — Districts dropdown
2.1 — Endpoint
GET /api/schools/districts?state=Maharashtra
Authorization: Bearer <token>
Rate limit: 60 / 60s.
Cache: Cache-Control: public, max-age=3600.
| Param | Type | Required | Notes |
|---|---|---|---|
state | string | yes | 1–80 chars. Case-insensitive equality match. |
400 if state missing: { "code": "MISSING_STATE" }.
Response:
{
"success": true,
"data": ["Ahmednagar", "Akola", "Amravati", "..."],
}
3 — School name typeahead
3.1 — Endpoint
GET /api/schools?q=delhi&state=&district=&pinCode=&limit=20
Authorization: Bearer <token>
Rate limit: 60 / 60s.
Cache: Cache-Control: public, max-age=300 — short, per-query.
| Param | Type | Required | Notes |
|---|---|---|---|
q | string | conditional | ≥ 2 chars. Case-insensitive prefix match on school name (startsWith). Required unless one of the filters below is provided. |
state | string | no | Case-insensitive equality. |
district | string | no | Case-insensitive equality. |
pinCode | string | no | Exact match on PIN string. |
limit | int | no | 1–50, default 20. |
At least one of q/state/district/pinCode is required — full-list
dumps are blocked (400 MISSING_FILTER).
q shorter than 2 chars → 400 QUERY_TOO_SHORT.
Response:
{
"success": true,
"data": [
{
"id": "01996c4a-...",
"udiseCode": "27130100102",
"name": "Delhi Public School Pune",
"state": "Maharashtra",
"district": "Pune",
"block": "Haveli",
"management": "Private Unaided",
"category": "Higher Secondary",
"pinCode": "411014",
},
],
}
Sorted alphabetically by name, tie-break by id.
3.2 — All the ways to call it
The three flows the UI supports — pick any:
A. Drill down (state → district → school)
GET /api/schools/states
GET /api/schools/districts?state=Maharashtra
GET /api/schools?state=Maharashtra&district=Pune&q=delhi&limit=20
Most narrow / fastest. State + district filters use the (state, district)
btree index.
B. Skip dropdowns, search by name
GET /api/schools?q=delhi&limit=20
Returns matches across all of India. Slower at large scale but acceptable for the prefix-on-name use case.
C. Pin code shortcut
GET /api/schools?pinCode=411014&limit=20
Useful when the user knows their PIN. Combine with q if needed.
3.3 — Auto-populating upper dropdowns from a school pick
Every school object carries state + district. When the user picks a
school via path B / C (without choosing state + district first), read those
fields off the selected row and write them into the state + district pickers:
const picked = result.data[i];
onboardingDraft.education.state = picked.state;
onboardingDraft.education.district = picked.district;
onboardingDraft.education.schoolId = picked.id;
onboardingDraft.education.schoolName = picked.name; // free-text fallback
4 — Re-hydrate a school by id
When restoring saved onboarding state (mobile sent schoolId previously and
wants to render the picker pre-filled):
GET /api/schools/01996c4a-...
Authorization: Bearer <token>
Same school object shape as section 3, wrapped as { success, data }.
404 if the id is unknown.
5 — Edge cases / error codes
| HTTP | code | Meaning |
|---|---|---|
| 400 | MISSING_FILTER | /api/schools called with no q and no filters. |
| 400 | QUERY_TOO_SHORT | q < 2 chars. |
| 400 | MISSING_STATE | /api/schools/districts called without state. |
| 401 | — | Token missing / invalid / expired. |
| 404 | — | /api/schools/:id — unknown id. |
| 429 | — | Rate limit hit. Back off using Retry-After header (seconds). |
6 — Sending the picked school back to onboarding
Persist both an opaque schoolId (FK candidate, picked from this catalog)
and the human-readable schoolName on the Education object you PATCH to
/api/onboarding/session:
PATCH /api/onboarding/session
{
"data": {
"education": [{
"schoolId": "01996c4a-...", // null if user typed a custom name
"schoolName": "Delhi Public School Pune",
"level": "high",
"grade": "10th",
"isCurrent": true,
"city": "Pune",
"country": "India"
}]
},
"checkpoint": "education"
}
Both schoolId and schoolName are persisted on the Education row:
schoolId— canonical FK into the School catalog (UDISE). Required if the user picked from the typeahead.schoolName— human-readable label, used for rendering on profile etc. Always required.
When the user typed a school not in the catalog, omit schoolId and
send only schoolName. The same shape is accepted by the standalone
POST/PUT/PATCH /api/users/me/education endpoints for post-signup edits.
7 — Quick reference cheat sheet
States dropdown → GET /api/schools/states
Districts dropdown → GET /api/schools/districts?state=<state>
School typeahead → GET /api/schools?q=<2+chars>&state=&district=&pinCode=&limit=<1-50>
School re-hydrate → GET /api/schools/:id
Auth header → Authorization: Bearer <onboardingToken | accessToken>