API overview
This page is for developers calling or extending the SBDMS API. It lists what exists today on production and the rules every endpoint follows. Controllers live under backend/Sbpdms.Backend/Features/, one folder per area.
Conventions that apply everywhere
Section titled “Conventions that apply everywhere”- All routes start with
/api/. In production Caddy forwards/api/*to the backend.GET /healthzandGET /readyzare the exceptions: no prefix, no auth. Swagger UI is at/swaggerin the Development environment only. - JSON in and out, camelCase keys. Deserialise case-insensitively; a few older responses use PascalCase.
- Send the JWT as
Authorization: Bearer <token>. Endpoints marked “public” below need no token. - Error shape. A refused request returns
{ "Error": "a sentence the user can act on" }. A few newer endpoints use lowercaseerror; the frontend’sapi.jsreads either. Unhandled exceptions come back as{ error, message, traceId }. - 404, not 403. When a scoped user (LocationAdmin, GeneralUser) asks for a row outside their camp sites, the server answers
404 Not Found, never403, so the caller cannot learn the row exists. Wrong role for an action is a plain403from[Authorize(Roles = ...)]. - List endpoints take
page,pageSize(capped at 200) and oftenquery. - Every write to a compliance table also writes an
AuditLogrow. See Data model.
Authentication
Section titled “Authentication”| Method and route | What it does |
|---|---|
POST /api/auth/login |
Username and password in, { token, username, name, role, code, campSiteIds, campSiteNames } out. 401 for a bad password or a disabled account. |
Public lookups
Section titled “Public lookups”Prefix /api/lookups. No token. These feed the self-registration form and are cached in memory for a short time; every admin write to the same table clears the cache. Routes: GET regions, GET districts, GET local-levels (filterable by parent id), GET campsites, GET campsites/{id} (never cached, so a freshly printed QR poster works at once) and GET exclusion-criteria.
Registration
Section titled “Registration”Prefix /api/registration.
| Route | What it does |
|---|---|
POST /api/registration |
Register a guardian with one or more children (public). A staff token, if present, only records who took the registration. With isVerification: true it marks existing records Verified instead. Returns 409 with the existing records when the same guardian and child already exist. |
PUT /api/registration/{id}/verify |
Staff mark one child verified and stamp the verifying site. |
Participants
Section titled “Participants”Prefix /api/participants, all authenticated, all row-scoped.
| Route | What it does |
|---|---|
GET |
Search children and guardians. Filters: query, campSiteId, myCampsiteOnly, showUnverified, includeGuardians, includeProfile, campDayId. When campDayId is a day the caller may run and query is an exact UPC, the site scope is lifted for that one child. |
GET {id} |
One child or guardian profile (?type=guardian). |
PUT {id}, DELETE {id} |
Edit or soft-delete. Delete is global admins only. |
GET {id}/guardians, GET guardian/{id}/children |
Links in either direction. |
POST {childId}/guardians, POST {childId}/link/{guardianId}, DELETE {childId}/guardians/{guardianId}, PUT {childId}/guardians/{guardianId}/primary |
Manage a child’s guardians. |
GET/POST {childId}/remarks, GET/POST {childId}/medical-history |
Monthly follow-up notes; append-only medical notes. |
PUT {id}/verification-campsite |
Change the site a child was verified at (global admins only). |
POST site-mismatch |
Record the decision a staff member made when warned that the child’s preferred site differs from the current one. Best-effort; never blocks. |
The dose-session API
Section titled “The dose-session API”The controller that runs the camp-day flow keeps a legacy route prefix for compatibility with existing clients; it is not printed here. Its endpoints, in the order a camp day uses them:
| Endpoint | What it does |
|---|---|
| start | Begin screening. Creates a Started row, or returns the open Started row for the same child and camp day so a second device resumes instead of duplicating. Needs an accompanying guardian or an “Other” name and relation. |
| submit | Finish as Completed (dose given) or FilteredOut (excluded). With a sessionId it updates that row; without one it inserts a new row (the two-row pattern in Data model). On Completed it also inserts the per-dose record. |
| sessions | All sessions for one child (childId, optional campDayId), newest first: the dose history. |
| session/{id} | One session in full. |
| incomplete | The screened-but-not-dosed rows for a camp day (campDayId). |
| complete-incomplete | Mark up to 500 of those rows Completed in one call, with the batch and the camp day’s date as dose date. Returns completed ids and skipped ids with reasons. |
Rules enforced by submit (and, where relevant, complete-incomplete), all returning 400 with an Error message:
- One dose per child per camp day. If a
Completedrow already exists for that child and day, the request is refused and the message names who gave the first dose, when and where. Why: two workers on two phones cannot see each other, so only the server can catch the double entry. - 60-dose lifetime cap.
PriorDoseCountplus completed sessions must be below 60 before another dose is written. - No future dose dates on paper entries. When
isPostPrashanEntryis true anddoseDateis later than today in Nepal time, the request is refused. Why: a date picker once offered “tomorrow” and hundreds of records landed on a day that had not happened.
There is no idempotency key on production. A retried submit after a network drop is caught by the one-dose-per-day rule, and a retried start simply resumes the open Started row.
Prefix /api/qr. POST confirm is public: the family’s phone posts the staff member’s code plus the scanned entity id and type (Child or Guardian), creating a five-minute handshake row. GET poll is authenticated: the staff screen picks up its newest unconsumed scan and the row is marked consumed.
Dashboard and analytics
Section titled “Dashboard and analytics”GET /api/dashboard/stats?campSiteId=&campDayId= returns the scoped headline counts. GET /api/analytics/camp-day/{campDayId} returns one day in full: totals, excluded, completed, screened-not-dosed, staff involved, timings, age and gender breakdowns.
All under /api/admin/. Reads are open to any staff (scoped); writes are role-gated as noted.
| Prefix | Endpoints | Notes |
|---|---|---|
campsites |
list, get, create, update, delete, POST {id}/assign-hpv |
Writes: global admins. |
hpv |
list (campsiteId filter), get, create, update, delete |
LocationAdmin may create only GeneralUsers at their own sites. SuperAdmin cannot be created here. The list hides SuperAdmin, GeneralAdmin and internal accounts. |
dosepreps |
list, get, create, update, POST {id}/lock, POST {id}/unlock, delete |
Create needs a preparer assigned to that site. Unlock: global admins. |
campdays |
list (campsiteId), GET deleted, get, create, update, POST {id}/complete, delete, POST {id}/restore, POST {id}/unassign-doses, GET {id}/audits, GET my-history |
Create returns the id in the Location header. List is { data: [...] }; deleted is a bare array. |
regions, districts, local-levels, units, ingredients, exclusioncriteria, doserules |
list, get, create, update, delete; doserules also GET recommend?ageMonths= |
Writes: global admins. |
imports |
GET {kind}/template, POST {kind}/validate, POST {kind}/commit, GET jobs/{id} where {kind} is participants or staff |
Two-phase CSV import, max 10,000 rows. Validate is a dry run; commit starts a background job you poll. Global admins. |
reports |
GET children, POST anusuchi-2 (XLSX or PDF), GET anusuchi-3, POST anusuchi-3/export, GET registrations, GET non-preferred-site/{campDayId}, POST children-advanced |
Anusuchi: global admins and LocationAdmin (scoped). Registrations, non-preferred-site: global admins. Advanced export: SuperAdmin. |
exports |
GET children, GET sessions, GET audit (from, to, `format=csv |
xlsx`) |
audit |
GET with table, pk, action, actor, fromAt, toAt, paging |
Read the audit log. Global admins. |
maintenance |
POST clear-tables |
SuperAdmin only. Destructive. |
Organisation rosters
Section titled “Organisation rosters”Prefix /api/org, authenticated. GET sources, GET sources/{id} and POST sources manage the organisations that bring rosters; GET campsites lists the sites the caller may import into; POST sources/{id}/import/preview classifies roster rows as new or existing without writing; POST sources/{id}/import/commit writes the batch and pulls the children into the main tables with Status = 'Org Verified'; GET sources/{id}/children returns the latest batch only. Writes need LocationAdmin or a global admin scoped to the source’s site. Rows are capped at 2,000.