Skip to content

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.

  • All routes start with /api/. In production Caddy forwards /api/* to the backend. GET /healthz and GET /readyz are the exceptions: no prefix, no auth. Swagger UI is at /swagger in 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 lowercase error; the frontend’s api.js reads 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, never 403, so the caller cannot learn the row exists. Wrong role for an action is a plain 403 from [Authorize(Roles = ...)].
  • List endpoints take page, pageSize (capped at 200) and often query.
  • Every write to a compliance table also writes an AuditLog row. See Data model.
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.

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.

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.

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 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 Completed row 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. PriorDoseCount plus completed sessions must be below 60 before another dose is written.
  • No future dose dates on paper entries. When isPostPrashanEntry is true and doseDate is 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.

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.

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.