NHCX Claims, Facility Settings, Reports, Notifications, Realtime & Logs
This document covers the parts of the orvo-abha backend that are not about consent/HIP-linking/health-document exchange: insurance claims processing (NHCX), facility-level ABDM configuration, operator reporting, the facility notification bell, the real-time (Pusher) channel-resolution pattern, and the internal logging endpoints. Every endpoint below is described from both a plain-English "what is this for" angle and a technical integration angle, with request/response examples taken directly from the actual Zod schemas and service code (not invented).
All routes are mounted in src/app.ts. Unless stated otherwise, JSON responses use the standard envelope:
{
"success": true,
"message": "Success",
"data": { "...": "..." },
"requestId": "b8c3968b-cf3d-4fee-acae-0f173724a70e"
}
NHCX Claims
What this is for (plain English): NHCX (National Health Claims Exchange) is India's ABDM module for cashless/insurance workflows — checking whether a patient's insurance covers a treatment, asking an insurer to pre-approve a procedure, submitting the actual claim after treatment, and receiving payment notifications. This is ABDM's "M4" milestone. Every one of these is asynchronous: our server submits a FHIR request to NHCX and gets only an acknowledgement back; the insurer's real decision arrives minutes/hours later as a separate inbound callback that ABDM's gateway calls on us.
Source: src/modules/abdm/hip/nhcx/ (nhcx.routes.ts for outbound submission, nhcx.callback.routes.ts for inbound insurer/gateway callbacks, nhcx.schema.ts, nhcx.controller.ts, nhcx.callback.controller.ts, nhcx.service.ts, nhcx.docs.ts).
Mounted at:
app.use('/v1/nhcx', writeLimiter, nhcxRouter)— outbound endpoints, rate-limited (60 req/15min).app.use('/', nhcxCallbackRouter)— inbound callbacks, mounted at the bare root with no rate limiting, because ABDM/the insurer is the caller, not our own frontend.
All four flows share one internal record type (NhcxClaim in Postgres, one row covers eligibility-checks, pre-auths, and claims) keyed by a correlationId that we generate on submission and that the insurer echoes back in their callback so we can match the response to the original request. Every callback also broadcasts a Pusher event on the facility's channel (see the Realtime section) so the operator dashboard updates live.
Outbound: Coverage Eligibility Check
POST /v1/nhcx/coverage-eligibility/check — asks an insurer "is this patient covered for this treatment?" before care is given.
- Who calls this: Our own frontend (orvo-hub/orvo-web operator dashboard), authenticated via the
X-HIP-IDheader (requireXHipIdmiddleware — 401 if missing). The HIP id is used purely to resolve the internalfacilityIdthat owns the resulting record. - Triggers: A facility operator decides to check coverage before scheduling a procedure.
- What happens next: The server builds a FHIR
CoverageEligibilityRequestbundle, creates aPENDINGNhcxClaimrow (claimType: COVERAGE_ELIGIBILITY), and posts the bundle to NHCX vianhcxClient. It returns immediately with202and aclaimId/correlationId. The actual eligibility decision (eligible/not, benefit amount, covered procedures) arrives later via the coverage-eligibility callback below, which updates the same row and fires anhcx:eligibility-checkedevent on the facility's Pusher channel.
Example request body (fields from CoverageEligibilityCheckSchema):
{
"abhaAddress": "iampawan@sbx",
"hipId": "IN0310000702_1",
"insurerId": "insurer.star-health@nhcx",
"subscriberId": "SUB123456",
"insurerName": "Star Health Insurance",
"policyNumber": "POL-2026-998877",
"serviceDate": "2026-04-29"
}
Example response (202 Accepted):
{
"claimId": "c1a2b3c4-d5e6-4f78-9012-3456789abcde",
"correlationId": "a9b8c7d6-e5f4-4321-9876-fedcba098765"
}
Outbound: Submit Pre-Authorization
POST /v1/nhcx/preauth/submit — asks an insurer to pre-approve a planned procedure before it's performed, e.g. for a scheduled surgery.
- Who calls this: Our frontend,
X-HIP-IDrequired. - Triggers: A doctor/operator has decided on a procedure and diagnosis and wants insurer sign-off in advance (typical for cashless hospitalization).
- What happens next: Builds a FHIR
ClaimBundle(use: preauthorization) viabuildClaimBundle(fhir/claim-bundle.builder.ts) — aClaimplus the Patient/Practitioner/Organization(provider)/Organization(insurer)/Coverage resources it references byreference, withBundle.identifier/timestampandentry.fullUrlon every entry, per the NRCeS ClaimBundle profile. Procedure entries with anamountalso become pricedClaim.itemlines (productOrService/net); those without one still appear inClaim.procedureonly. Creates aPENDINGNhcxClaimrow (claimType: PREAUTHORIZATION), submits it, and returns202immediately. The insurer's decision (APPROVED/DENIED/PARTIAL/PENDING, approved amount, remarks) arrives via the pre-auth callback, updating the row and emittingnhcx:preauth-updated.
Example request body (PreauthorizationSubmitSchema):
{
"abhaAddress": "iampawan@sbx",
"hipId": "IN0310000702_1",
"insurerId": "insurer.star-health@nhcx",
"careContextReference": "ENC-2026-00123",
"procedures": [
{
"system": "http://snomed.info/sct",
"code": "80146002",
"display": "Appendectomy",
"amount": 45000
}
],
"diagnoses": [
{ "system": "http://snomed.info/sct", "code": "74400008", "display": "Appendicitis" }
],
"estimatedCost": 45000,
"supportingDocumentIds": ["doc-8891"]
}
procedures[].amount is optional — when omitted, Claim.total (from estimatedCost) is still sent but the procedure has no priced Claim.item line for the insurer to adjudicate individually.
Example response (202 Accepted):
{
"claimId": "c1a2b3c4-d5e6-4f78-9012-3456789abcde",
"correlationId": "a9b8c7d6-e5f4-4321-9876-fedcba098765"
}
POST /v1/nhcx/preauth/{claimId}/enhance and POST /v1/nhcx/preauth/{claimId}/resubmit — continue an existing pre-authorization case (extra days/cost, or after a payer query/rejection) rather than starting a new one. Same request body shape as submit (PreauthorizationSubmitSchema, full re-specification, not a diff), same NhcxClaim row and x-hcx-workflow_id as the original request (so the payer sees one continuous case), but a fresh correlationId and x-hcx-use_case set to Preauthorization Enhancement / Preauthorization Resubmit respectively. 404 if claimId isn't this facility/patient's; 422 if it's already APPROVED.
Outbound: Submit Claim
POST /v1/nhcx/claim/submit — submits the actual reimbursement claim to the insurer after treatment has been given, optionally linked to an earlier approved pre-authorization.
- Who calls this: Our frontend,
X-HIP-IDrequired. - Triggers: Treatment/discharge is complete and the facility wants to bill the insurer.
- What happens next: Builds the same
ClaimBundleshape as pre-authorization (use: claim) viabuildClaimBundle, optionally referencingpreauthorizationIdviaClaim.related— currently our own internal claim id, not the payer's own pre-auth reference, since that's never parsed back from the payer'sClaimResponse(see the operations notes). Creates aPENDINGNhcxClaimrow (claimType: CLAIM), submits it, returns202. The claim decision (approved/denied/pending/queued/partial, approved amount, remarks) arrives via the claim callback (nhcx:claim-updatedevent); a subsequent, separate payment notification records the actual payout.
Example request body (ClaimSubmitSchema):
{
"abhaAddress": "iampawan@sbx",
"hipId": "IN0310000702_1",
"insurerId": "insurer.star-health@nhcx",
"preauthorizationId": "c1a2b3c4-d5e6-4f78-9012-3456789abcde",
"careContextReference": "ENC-2026-00123",
"procedures": [
{ "system": "http://snomed.info/sct", "code": "80146002", "display": "Appendectomy" }
],
"diagnoses": [
{ "system": "http://snomed.info/sct", "code": "74400008", "display": "Appendicitis" }
],
"claimAmount": 45000,
"supportingDocumentIds": ["doc-8891", "doc-8892"]
}
Example response (202 Accepted):
{
"claimId": "d2b3c4d5-e6f7-4890-1234-56789abcdef0",
"correlationId": "b0c9d8e7-f6a5-5432-1098-765fedcba109"
}
POST /v1/nhcx/claim/{claimId}/resubmit — resubmits a queried or partially-rejected claim, continuing the claim's own case (x-hcx-use_case: Claim Resubmit). Note this reuses the claim's own x-hcx-workflow_id, not the originating pre-authorization's — by resubmission time the two have diverged. Same 404/422 semantics as the pre-authorization enhance/resubmit endpoints above.
POST /v1/nhcx/claim/{claimId}/task — the 'Reprocess' use case (POST /v1/task/submit): action: "reprocess" asks the payer to readjudicate a partially approved/rejected claim (x-hcx-use_case: Claim Reprocess); "cancel"/"release"/"nullify" withdraw or close it (no x-hcx-use_case — the spec's use-case value set only covers reprocess). An optional reasonCode (partialpayment, erroneousclaim, claimrejected, referred, erroneousregistration, wrongdiagnosis, treatmentplanchanged) maps to Task.reasonCode. Like /status and /resubmit, reuses the claim's own correlationId/workflowId. The payer's decision arrives via POST /v1/task/on_submit as a ClaimResponse — parsed and applied exactly like the ordinary claim callback (same status derivation, same nhcx:claim-updated event). 404/422 semantics match resubmit.
List Claims for a Facility
GET /v1/nhcx/{hipId}/claims — lists every NHCX record (eligibility checks, pre-auths, claims) for a facility, for a claims-tracking screen.
- Who calls this: Our frontend,
X-HIP-IDrequired (in addition to thehipIdpath param, which is used to resolve the owningfacilityId). - Query params (
NhcxClaimQuerySchema):limit(1–100, default 20),offset(default 0),claimType(COVERAGE_ELIGIBILITY|PREAUTHORIZATION|CLAIM, optional filter),status(PENDING|APPROVED|DENIED|QUEUED|PARTIAL|ERROR, optional filter). - What happens next: Pure read — queries
NhcxClaimfor the facility, newest first, with the requested filters/pagination.
Example: GET /v1/nhcx/IN0310000702_1/claims?limit=20&offset=0&claimType=CLAIM&status=PENDING
Example response (200):
{
"claims": [
{
"id": "d2b3c4d5-e6f7-4890-1234-56789abcdef0",
"claimType": "CLAIM",
"status": "PENDING",
"abhaAddress": "iampawan@sbx",
"insurerId": "insurer.star-health@nhcx",
"correlationId": "b0c9d8e7-f6a5-5432-1098-765fedcba109",
"careContextRef": "ENC-2026-00123",
"approvedAmount": null,
"remarks": null,
"createdAt": "2026-07-18T09:12:00.000Z",
"updatedAt": "2026-07-18T09:12:00.000Z"
}
]
}
Get a Single Claim
GET /v1/nhcx/claims/{claimId} — fetches full detail for one claim/pre-auth/eligibility-check record, including the outbound FHIR bundle sent and (once received) the insurer's raw response.
- Who calls this: Our frontend. Unlike the other NHCX routes, this one does not require
X-HIP-ID. - What happens next: Looks up the
NhcxClaimrow by internal id. Returns404if not found.
Example response (200) — abbreviated:
{
"id": "d2b3c4d5-e6f7-4890-1234-56789abcdef0",
"facilityId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"claimType": "CLAIM",
"status": "APPROVED",
"abhaAddress": "iampawan@sbx",
"hipId": "IN0310000702_1",
"insurerId": "insurer.star-health@nhcx",
"correlationId": "b0c9d8e7-f6a5-5432-1098-765fedcba109",
"careContextRef": "ENC-2026-00123",
"approvedAmount": 42000,
"remarks": "Approved with 3000 deductible",
"claimBundle": {
"resourceType": "Bundle",
"id": "b0c9d8e7-...",
"type": "collection",
"entry": ["..."]
},
"responsePayload": {
"correlationId": "b0c9d8e7-...",
"decision": "APPROVED",
"approvedAmount": 42000
},
"createdAt": "2026-07-18T09:12:00.000Z",
"updatedAt": "2026-07-18T09:20:00.000Z"
}
List Payments
GET /v1/nhcx/{hipId}/payments — lists payout/EOB (Explanation of Benefits) notifications received from insurers for a facility.
- Who calls this: Our frontend,
X-HIP-IDrequired. Query paramslimit/offsetare read directly offreq.query(not enforced by a Zod schema on this specific route), default 20/0. - What happens next: Pure read of the
NhcxPaymenttable for the facility, newest first.
Example response (200):
{
"payments": [
{
"id": "e3c4d5e6-f7a8-4901-2345-6789abcdef01",
"facilityId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"claimId": "d2b3c4d5-e6f7-4890-1234-56789abcdef0",
"correlationId": "b0c9d8e7-f6a5-5432-1098-765fedcba109",
"paymentStatus": "PAID",
"paidAmount": 42000,
"paymentDate": "2026-07-25",
"eobReference": "EOB-2026-9981",
"remarks": null,
"createdAt": "2026-07-25T11:00:00.000Z"
}
]
}
Communication (payer document requests)
A payer can ask mid-cycle for more documents or information on a claim/pre-auth (POST /v1/communication/request, the "Communication (additional docs)" use case). This can't be answered synchronously — which document satisfies the request is a facility operator's call, not something we can infer — so the inbound callback only persists an NhcxCommunicationRequest row (status: PENDING) and fires nhcx:communication-requested on the facility's Pusher channel; a person picks the document afterward.
GET /v1/nhcx/communication — lists NhcxCommunicationRequest rows for the facility. status (PENDING/RESPONDED/ERROR), limit/offset query params.
POST /v1/nhcx/communication/{requestId}/respond — attaches a previously uploaded document (documentId, same id space as supportingDocumentIds elsewhere) and an optional note, builds a Communication TaskBundle (fhir/communication-response.builder.ts) and sends it via POST /v1/communication/on_request, then marks the row RESPONDED and fires nhcx:communication-responded. 404 if requestId isn't this facility's; 422 if already answered or the document belongs to a different patient than the underlying claim.
Example response from GET /v1/nhcx/communication:
{
"requests": [
{
"id": "f4a5b6c7-d8e9-4012-3456-789abcdef012",
"facilityId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"claimId": "d2b3c4d5-e6f7-4890-1234-56789abcdef0",
"claimNumber": "CLAIM-2026-001",
"reasonReference": "Discharge summary required",
"status": "PENDING",
"createdAt": "2026-09-29T10:00:00.000Z"
}
]
}
Status (re-query a stuck request)
POST /v1/nhcx/status/{claimId} — a diagnostic nudge for a claim/pre-auth/eligibility-check that submitted fine (got its 202) but never got its on_submit/on_check decision callback. Sends the 'Status' use case (POST /v1/status, body is an empty string — not JSON) reusing the original request's correlationId/workflowId rather than minting new ones, since that's how the gateway knows which request to report on. The answer arrives async on POST /v1/on_status and only updates NhcxMessage's audit trail; it flips a still-PENDING claim to ERROR if the gateway reports a terminal protocol failure (response.error/request.error/request.stopped), but never sets APPROVED/DENIED itself. 404 if the claim isn't this facility's or never had an outbound request.
Both
/v1/statusand/v1/on_statusare the one pair of header tables in the whole spec that never listx-hcx-entity-type— every other flow does. Our shared protocol layer (core/nhcx.protocol.ts) treats it as optional for exactly this reason, so a real on_status callback lacking it still decrypts; the other five inbound handlers still enforce it themselves since their own header tables require it.
Inbound Callbacks (ABDM/insurer → us)
These eight endpoints are called by the ABDM gateway / insurer's NHCX system, not by our own frontend. They are mounted at the bare root (app.use('/', nhcxCallbackRouter)), with no rate limiting, and every request passes through createInboundCallbackLogger('nhcx') first — which hashes and stores the raw payload/headers for audit before the handler runs. Each handler immediately responds 202 and then processes the payload asynchronously in the background — this matters because NHCX expects a fast ack and will treat a slow response as a delivery failure.
The paths below are not ours to choose: NHCX registers one participant endpoint_url (the domain root, set via configure-nhcx-participant) and the gateway appends these exact spec-defined suffixes itself — see NHCX_PATHS in core/nhcx.endpoints.ts. Payment notice is the one flow the payer initiates, so its inbound path is .../request, not an on_* suffix.
The JSON examples below (
CoverageEligibilityOnCheckSchemaetc.) are illustrative only and predate the real NHCX FHIR envelope — actual inbound payloads are five-part compact JWEs decrypted server-side into a FHIR Bundle (seenhcx.service.tsand thefhir/parsers), not this flat JSON. This section needs a full rewrite against the real schemas; treat it as directional until then.
POST /v1/coverageeligibility/on_check — insurer's answer to a coverage-eligibility check.
Illustrative inbound payload shape (see the note above — not a real schema):
{
"correlationId": "a9b8c7d6-e5f4-4321-9876-fedcba098765",
"status": "processed",
"eligible": true,
"benefitAmount": 100000,
"coveredProcedures": ["80146002"]
}
What happens: looks up the NhcxClaim by correlationId, sets its status to APPROVED/DENIED based on eligible, stores the raw responsePayload, and — if a matching facility is found — fires nhcx:eligibility-checked on that facility's Pusher channel. If correlationId is missing, it just logs a warning and does nothing (no error surfaced to NHCX, since we already 202'd).
POST /v1/preauth/on_submit — insurer's decision on a pre-authorization request.
Illustrative inbound payload shape (see the note above — not a real schema):
{
"correlationId": "a9b8c7d6-e5f4-4321-9876-fedcba098765",
"decision": "APPROVED",
"approvedAmount": 40000,
"remarks": "Approved for 3-day admission"
}
What happens: updates the matching NhcxClaim's status/approvedAmount/remarks, stores the raw payload, fires nhcx:preauth-updated.
POST /v1/claim/on_submit — insurer's decision on a submitted claim.
Illustrative inbound payload shape (see the note above — not a real schema):
{
"correlationId": "b0c9d8e7-f6a5-5432-1098-765fedcba109",
"decision": "APPROVED",
"approvedAmount": 42000,
"remarks": "Approved with 3000 deductible"
}
What happens: same update pattern as pre-auth, fires nhcx:claim-updated. decision can additionally be QUEUED or PARTIAL for claims (unlike pre-auth). Status is derived from ClaimResponse.outcome and ClaimResponse.formCode (the payer-attested preauthapproval/claimapproval/preauthdenial/claimdenial codes) — never from the free-text disposition, which a payer can phrase however it likes (see parseClaimResponseBundle in fhir/claim-response.parser.ts).
POST /v1/paymentnotice/request — insurer notifies that a payment/EOB has been issued against a claim. The payload is a real FHIR Bundle (a PaymentNotice plus its referenced PaymentReconciliation, per the PaymentNotice sheet and the PaymentNotice profile) — PaymentNotice.amount (top-level Money) is the paid amount, and PaymentNotice.payment is a reference to the PaymentReconciliation entry that carries paymentDate and paymentIdentifier (the UTR). After persisting the NhcxPayment row, we fire-and-forget an acknowledgement back through the gateway at POST /v1/paymentnotice/on_request (x-hcx-status: response.complete, a Task payload with output: ACKNOWLEDGED/RECEIVED) — see sendPaymentNoticeAcknowledgement in nhcx.service.ts.
Illustrative inbound payload shape (see the note above — not a real schema):
{
"correlationId": "b0c9d8e7-f6a5-5432-1098-765fedcba109",
"claimId": "d2b3c4d5-e6f7-4890-1234-56789abcdef0",
"paymentStatus": "PAID",
"paidAmount": 42000,
"paymentDate": "2026-07-25",
"eobReference": "EOB-2026-9981",
"remarks": null
}
What happens: resolves the owning facilityId (directly, or via the NhcxClaim matched by correlationId if not supplied), creates a new NhcxPayment row, and fires nhcx:payment-notified on the facility's Pusher channel. If no facilityId can be resolved at all, the notification is dropped with a warning log (nothing to attach it to).
POST /v1/communication/request — insurer asks for more documents/information on a claim or pre-authorization. The payload is a TaskBundle (Task code=poll + a CommunicationRequest resource — identifier is the payer's claim number, basedOn references the claim, reasonReference names what they want). Unlike the other five, this is not answered inline: see the Communication section above — it persists a PENDING NhcxCommunicationRequest row and fires nhcx:communication-requested; a facility operator answers it later via POST /v1/nhcx/communication/{requestId}/respond, which sends the actual POST /v1/communication/on_request.
POST /v1/on_status — the gateway's async answer to our own diagnostic POST /v1/nhcx/status/{claimId} query. See the Status section above; this only updates NhcxMessage's audit trail (and flips a still-PENDING claim to ERROR on a terminal protocol failure), never the business decision.
POST /v1/task/on_submit — the payer's answer to a reprocess/cancel/release/nullify task (POST /v1/nhcx/claim/{claimId}/task, see above). The payload is a ClaimResponse, parsed with the exact same parseClaimResponseBundle the ordinary claim callback uses, so it applies status/approvedAmount/remarks the same way and fires the same nhcx:claim-updated event.
POST /v1/error — the gateway telling us one of our own outbound requests never reached its recipient, or was rejected outright, after 5 delivery attempts (per 'API Response Handling to avoid Failures.pdf' and 'Common Mistakes while implementing through NHCX.pdf' #2 — "every integrator should implement the v1/error API at their end"). NHCX deletes the correlation_id from its own system once this fires, so there's no later callback to wait for — arriving here at all is the terminal signal, and a still-PENDING claim is unconditionally flipped to ERROR. The raw x-hcx-error_details.code (PAYR-* from the payer, NHCX-* from the gateway itself) is looked up in nhcx-error-codes.ts's catalogue and, when known, its fuller description replaces NHCX's own often-terse message on both the NhcxMessage row and the claim's remarks — the catalogue also classifies each code as Transport (worth retrying — cert/connectivity) or Business (won't fix itself — a data mismatch), logged alongside the warning.
Unlike every other endpoint on this page, no Postman collection or OpenAPI file in the spec bundle documents
/v1/error's exact wire shape — only PDF prose, with a sample body ("Protocol Response in case of error scenarios") whose keys are thex-hcx-*protected-header names directly at the top level, unlike every other flow's{"payload": "<JWE>"}envelope.handleErrorNotification/NhcxErrorNotificationSchemaimplement that literal reading (plain JSON, not JWE-wrapped) — treat this one as the least certain of the inbound callbacks until it's been exercised against a real gateway.
Facility Settings
What this is for (plain English): Every facility (hospital/clinic) using this platform needs a place to configure how it identifies itself to ABDM (its HIP/HIU ids), how sensitive its different document types are treated, its default consent rules, and — since ABDM sandbox credentials are shared per environment — the gateway client credentials themselves. This module is that configuration surface.
Source: src/modules/abdm/hip/settings/ (settings.routes.ts, settings.controller.ts, settings.service.ts, settings.docs.ts). Mounted at app.use('/v1/settings', generalLimiter, settingsRouter).
A key architectural note baked into this module: the backend now runs a single shared ABDM bridge/client id across all facilities (see the hiuId/gateway-credentials handling below) — only one facility is "live" for HIU purposes at a time, selected via "active facility."
Get Facility Settings
GET /v1/settings/facility — returns one facility's ABDM identity and configuration.
- Who calls this: Our frontend (operator settings screen). Facility resolution:
X-HIP-IDheader →HIP_IDenv var → whichever facility is currentlyactive. - What happens next: Reads the
Facilityrow plus itsFacilitySettingsrow plus the system-wide HIU id. IfFacilitySettingswas never written for this facility, sensible defaults are returned instead of erroring (empty classification lists, 90-day consent expiry, auto-approve off, verification required).
Example response (200):
{
"success": true,
"message": "Success",
"data": {
"settings": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"facilityId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"facilityName": "Orvo Multispeciality Hospital",
"registrationNumber": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"dataClassification": {
"public": ["OPConsultation"],
"internal": ["DischargeSummary", "Prescription"],
"confidential": ["WellnessRecord"]
},
"consentDefaults": {
"defaultExpiryDays": 5,
"autoApproveConsents": false,
"requirePatientVerification": true
},
"hipId": "IN0310000702_1",
"hiuId": "IN0310000702_1",
"roleIdentityMappings": null,
"isActive": true,
"updatedAt": "2026-01-15T10:30:00.000Z"
}
},
"requestId": null
}
Update Facility Settings
PATCH /v1/settings/facility — partially updates a facility's name, HIP id, HIU id, data classification, consent defaults, and/or role-identity mappings.
- Who calls this: Our frontend. Same facility resolution as GET.
- Field routing (important for integrators):
facilityName/hipIdare written straight onto theFacilityrow and are trimmed server-side — a stray trailing space inhipIdmakes ABDM silently drop consents/callbacks later (this bit us before).hiuIdis not per-facility — it's written to the sharedBridgeConfigbecause a single bridge now backs every facility.dataClassification/consentDefaults/roleIdentityMappingsare upserted intoFacilitySettings. Every field is optional; only supplied fields change. - What happens next: Returns the refreshed settings object (same shape as GET).
Example request body:
{
"facilityName": "Orvo Multispeciality Hospital",
"hipId": "IN0310000702_1",
"dataClassification": {
"public": ["OPConsultation"],
"internal": ["DischargeSummary", "Prescription"],
"confidential": ["WellnessRecord"]
}
}
Response: identical shape to GET /v1/settings/facility.
List Facilities
GET /v1/settings/facilities — lightweight list of every registered facility, for an active-facility picker in the settings UI.
- Who calls this: Our frontend.
- What happens next: Reads all
Facilityrows (id,name,hipId,active).
Example response (200):
{
"success": true,
"message": "Success",
"data": {
"facilities": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Orvo Multispeciality Hospital",
"hipId": "IN0310000702_1",
"active": true
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"name": "Orvo Clinic Deprecated",
"hipId": "IN0310000702_2",
"active": false
}
]
},
"requestId": null
}
Set Active Facility
PATCH /v1/settings/facility/active — designates exactly one facility as "the one we transact as by default" when no X-HIP-ID is supplied.
- Who calls this: Our frontend, an admin action.
- What happens next: In a single DB transaction, sets
active: trueon the givenfacilityIdandactive: falseon every other facility — deliberately modeled as "exactly one active facility" so that fallback logic elsewhere (e.g.resolveFacilityForConsent) is deterministic rather than racing across multiple active rows. Returns404iffacilityIddoesn't match anything.
Example request body (SetActiveFacilitySchema):
{ "facilityId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
Response: same shape as GET /v1/settings/facilities (the refreshed list).
Get / Update Gateway Credentials
GET /v1/settings/credentials — shows whether ABDM gateway client credentials (client id/secret) are configured for sandbox and production, without exposing the secret itself.
- Who calls this: Our frontend, admin-only in intent (not currently enforced by role middleware at the route level).
- What happens next: For each environment, resolves the
bridgeId(fromABDM_BRIDGE_IDenv, else the active facility'sbridgeIdfor that environment, else any facility's), then looks up the storedBridgeConfigrow.
Example response (200):
{
"success": true,
"message": "Success",
"data": {
"credentials": {
"sandbox": {
"environment": "sandbox",
"bridgeId": "IN0310000702",
"clientId": "sbx_client_abc123",
"hasClientSecret": true,
"updatedAt": "2026-01-10T08:00:00.000Z"
},
"production": {
"environment": "production",
"bridgeId": null,
"clientId": null,
"hasClientSecret": false,
"updatedAt": null
}
}
},
"requestId": null
}
PATCH /v1/settings/credentials — sets or rotates the ABDM client id/secret for one environment.
- Who calls this: Our frontend, admin action.
- Requirements:
environmentmust besandboxorproduction(422 otherwise); at least one ofclientId/clientSecretmust be provided;bridgeIdis optional (auto-resolved if omitted, and the call 422s if it still can't be resolved). - What happens next: Upserts (creates or updates) the
BridgeConfigrow for thatbridgeId/environment pair. On create, falls back toABDM_M234_CLIENT_ID/ABDM_M234_CLIENT_SECRETenv vars if not both supplied in the body. Returns the refreshed credentials object (same shape as GET).
Example request body:
{
"environment": "sandbox",
"clientId": "sbx_client_abc123",
"clientSecret": "s3cr3t-value-not-echoed-back"
}
Update Data Classification
PATCH /v1/settings/data-classification — sets which document types count as public/internal/confidential for a facility.
- Who calls this: Our frontend.
- What happens next: Replaces (not merges) the
dataClassificationobject wholesale onFacilitySettings(upserted if it doesn't exist yet).
Example request body:
{
"dataClassification": {
"public": ["OPConsultation"],
"internal": ["DischargeSummary", "Prescription"],
"confidential": ["WellnessRecord"]
}
}
Example response (200):
{ "success": true, "message": "Success", "data": { "updated": true }, "requestId": null }
Update Consent Defaults
PATCH /v1/settings/consent-defaults — sets the default consent expiry window and approval behavior used when pre-filling consent-init requests for a facility.
- Who calls this: Our frontend.
- What happens next: Replaces the whole
consentDefaultsobject (all three fields must be supplied together — it's stored/overwritten as one JSON blob), upserted intoFacilitySettings.
Example request body:
{
"consentDefaults": {
"defaultExpiryDays": 90,
"autoApproveConsents": false,
"requirePatientVerification": true
}
}
Response: { "updated": true } in the standard envelope, as above.
Feature Flags
What this is for (plain English): a tiny remote kill-switch/rollout table so orvo-phr, orvo-web, and orvo-hub can each ask "which of my features should be on right now" without a redeploy. There is no per-facility or per-user targeting — a flag is on or off for one app in one ABDM environment (sandbox/production), globally.
Backed by a single FeatureFlag table, unique on (key, app, environment). environment is not taken from the caller — it's resolved server-side via currentAbdmEnvironment() (the same helper resolveSystemHiuId uses), so a client never has to know or declare whether it's pointed at sandbox or production.
GET /v1/feature-flags?app=orvo-hub
The public read endpoint every client app calls at boot.
- Caller:
orvo-phr/orvo-web/orvo-hub. No auth — a flag's on/off state isn't sensitive, and every caller here is itself a public client app, so gating this behind login would just mean shipping the same list a different way. - Query:
app— one oforvo-phr,orvo-web,orvo-hub(FEATURE_FLAG_APPS; adding a new app means adding it to that list infeature-flags.schema.ts). - Response (
200):
{ "success": true, "data": { "flags": ["nhcx", "health-locker-v2"] } }
Only enabled flag keys for that app+environment are returned — a disabled or nonexistent flag simply doesn't appear in the array. This list is the only source of flag state for client apps — a flag not returned is off.
GET /v1/admin/feature-flags
Lists every flag row across every app and environment — the source for the orvo-super-admin ops table.
- Caller:
orvo-super-admin. No auth of its own — same posture as/v1/settings,/v1/realtime/ops-channel, and/v1/user-linking/callback-traces: this backend relies entirely on super-admin's own app-level auth to gate who reaches the page that calls it. - Response (
200):{ "success": true, "data": { "flags": [{ "id": "...", "key": "nhcx", "app": "orvo-hub", "environment": "sandbox", "enabled": true, "updatedAt": "2026-09-18T05:38:15.000Z" }] } }
PUT /v1/admin/feature-flags
Creates or toggles one (key, app, environment) row.
- Request body:
{ "key": "nhcx", "app": "orvo-hub", "environment": "production", "enabled": false }
key must be lowercase letters/digits/hyphens only (/^[a-z0-9-]+$/). The row is upserted on the (key, app, environment) unique constraint — a first call for a new key creates it, a later call just flips enabled.
- Response (
200): the updated flag row, same shape as the admin list entry above.
Reports
What this is for (plain English): These are dashboard/analytics endpoints for facility operators and admins — how many consents were requested vs. granted, how successful care-context linking has been, how often insurers/HIUs are pulling data, which document types are most requested, and day-by-day activity trends. Nothing here writes data; it's all read-only aggregation over existing tables.
Source: src/modules/abdm/hip/reports/ (reports.routes.ts, reports.controller.ts, reports.service.ts, reports.docs.ts). Mounted at app.use('/v1/reports', generalLimiter, reportsRouter).
All endpoints share the same facility scoping rule: they're scoped to the facility resolved from X-HIP-ID (falling back to HIP_ID env, then the active facility); if none can be resolved, the report is computed across all facilities rather than erroring — worth knowing since a missing header silently changes the scope of the numbers rather than failing loudly.
Consent Metrics
GET /v1/reports/consent-metrics — approval rate and consent volume breakdown by status.
- Who calls this: Our frontend (reports dashboard),
X-HIP-IDoptional (see scoping note above). - Query params:
startDate,endDate(both optional ISO date/time, filter by consentcreatedAt). - What happens next: Counts consents by status (
GRANTED/REQUESTED/DENIED/REVOKED/EXPIRED), computesapprovalRate(% granted of total, rounded) andaverageExpiryDays(average days between a granted consent'sfromDateand its artefact'sdataEraseAt, across consents that have both values).
Example: GET /v1/reports/consent-metrics?startDate=2026-06-01T00:00:00.000Z&endDate=2026-07-01T00:00:00.000Z
Example response (200, data payload):
{
"metrics": {
"totalRequests": 120,
"approvedConsents": 90,
"pendingConsents": 10,
"deniedConsents": 8,
"revokedConsents": 5,
"expiredConsents": 7,
"approvalRate": 75,
"averageExpiryDays": 88
}
}
Linking Metrics
GET /v1/reports/linking-metrics — how well care-context linking (attaching a patient's encounter to their ABHA) is going.
- Who calls this: Our frontend. Same
startDate/endDatequery params. - What happens next: Counts
CareContextrows and how many havelinked: true, computeslinkingSuccessRate.
Example response data:
{
"metrics": {
"totalPatients": 50,
"linkedPatients": 45,
"activeLinkages": 45,
"failedLinkages": 5,
"linkingSuccessRate": 90
}
}
Data Access Metrics
GET /v1/reports/data-access-metrics — how often HIUs are actually pulling health information data, and how often that succeeds.
- Who calls this: Our frontend. Same date-range query params (filtered by
requestedAt). - What happens next: Counts
HealthInformationRequestrows by outcome (COMPLETED/FAILED), counts distinct HIU ids that hold a consent for the facility, and finds the most recent access timestamp.
Example response data:
{
"metrics": {
"totalAccessRequests": 200,
"successfulAccess": 180,
"failedAccess": 15,
"uniqueHIUs": 4,
"lastAccessDate": "2026-07-13T09:15:00.000Z"
}
}
Top Data Types
GET /v1/reports/top-data-types — which health information types (OPConsultation, DiagnosticReport, etc.) are requested most often, all-time (not date-filtered).
- Who calls this: Our frontend. Query param:
limit(defaults to 5). - What happens next: Groups
CareContextrows byhiType, returns top N by count, descending.
Example response data:
{
"topDataTypes": [
{ "name": "OPConsultation", "count": 42 },
{ "name": "DiagnosticReport", "count": 31 }
]
}
Top HIUs
GET /v1/reports/top-hius — which HIUs (insurers, other hospitals, etc.) are most actively holding consents against this facility's data, all-time.
- Who calls this: Our frontend. Query param:
limit(defaults to 5). - What happens next: Groups
Consentrows byhiuId(excluding null), returns top N by consent count.
Example response data:
{
"topHIUs": [
{ "name": "IN0310000702_1", "accessCount": 37 },
{ "name": "insurer.star-health@nhcx", "accessCount": 12 }
]
}
Daily Trends
GET /v1/reports/daily-trends — day-by-day counts of consents created, care-context links, and data-access requests, for a trend chart.
- Who calls this: Our frontend. Query params
startDate/endDatedefault to the last 30 days ending now if omitted. - What happens next: Buckets each of the three event types by calendar date (UTC). Only dates with at least one event appear (no zero-filled gaps).
Example response data:
{
"dailyTrends": [
{ "date": "2026-07-10", "consents": 6, "links": 4, "access": 9 },
{ "date": "2026-07-11", "consents": 3, "links": 2, "access": 5 }
]
}
Comprehensive Report
GET /v1/reports/comprehensive — all of the above combined into a single dashboard payload, computed with 6 parallel queries instead of 6 round-trips.
- Who calls this: Our frontend, for a single dashboard-load fetch. Same
startDate/endDatedefaulting as daily-trends. - What happens next: Runs consent-metrics, linking-metrics, data-access-metrics, top-data-types (limit 5), top-HIUs (limit 5), and daily-trends concurrently and merges the results.
Example response data (abbreviated):
{
"dateRange": { "startDate": "2026-06-14T00:00:00.000Z", "endDate": "2026-07-14T00:00:00.000Z" },
"consentMetrics": { "totalRequests": 120, "approvedConsents": 90, "...": "..." },
"linkingMetrics": { "totalPatients": 50, "linkedPatients": 45, "...": "..." },
"dataAccessMetrics": { "totalAccessRequests": 200, "...": "..." },
"topDataTypes": [{ "name": "OPConsultation", "count": 42 }],
"topHIUs": [{ "name": "IN0310000702_1", "accessCount": 37 }],
"dailyTrends": [{ "date": "2026-07-10", "consents": 6, "links": 4, "access": 9 }]
}
Facility Notifications
What this is for (plain English): This powers the notification bell in the facility operator dashboard — things like "a patient granted consent," "a subscription changed status," "health data was pushed." These rows are written locally by our own ABDM callback handlers as events happen; this module does not proxy anything live from ABDM. (Note: there are two sibling modules elsewhere with a similar shape — abdm/phr/gateway-notifications, a live proxy straight to ABDM's own notification feed for a patient, and abdm/phr/patient-notifications, the patient-side twin of this same Notification table. Neither is covered here.)
Source: src/modules/facility-notifications/ (facility-notifications.routes.ts, .controller.ts, .service.ts, .schema.ts, .docs.ts). Mounted at app.use('/v1/notifications', generalLimiter, facilityNotificationsRouter).
List Notifications
GET /v1/notifications — paginated notification feed for the calling facility's bell.
- Who calls this: Our frontend (facility operator dashboard), authenticated via
X-Facility-IDheader (requireFacilityId— 400 if missing and no session facility id either). - Query params (
ListNotificationsQuerySchema):limit(1–100, default 50),offset(default 0),unreadOnly(pass the literal string"true"to filter to unread only). - What happens next: Reads the
Notificationtable filtered byfacilityId, newest first, and also returns a runningunreadCountfor the bell badge.
Example: GET /v1/notifications?limit=20&offset=0&unreadOnly=true
Example response data:
{
"notifications": [
{
"id": "ntf_9f1c2b4a",
"facilityId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "CONSENT_GRANTED",
"title": "Consent granted",
"message": "Patient iampawan@sbx granted consent for OP consultation records",
"isRead": false,
"readAt": null,
"createdAt": "2026-07-18T08:00:00.000Z"
}
],
"total": 132,
"unreadCount": 4
}
Mark One Notification Read
PATCH /v1/notifications/{id}/read — dismisses a single bell item.
- Who calls this: Our frontend,
X-Facility-IDrequired. - What happens next: Sets
isRead: trueandreadAton the row, scoped by bothidandfacilityIdin a singleupdateMany— a notification id belonging to a different facility silently matches zero rows rather than erroring (no 404 semantics here).
Example: PATCH /v1/notifications/ntf_9f1c2b4a/read (no body).
Example response: { "success": true, "message": "Success", "data": null, "requestId": null }
Mark All Notifications Read
POST /v1/notifications/read-all — clears the whole bell in one action ("mark all as read").
- Who calls this: Our frontend,
X-Facility-IDrequired. No body/filters accepted — it always targets every currently-unread row for the resolved facility. - What happens next: Bulk
updateManysettingisRead: true/readAtfor all unread rows of that facility.
Example response: { "success": true, "message": "Success", "data": null, "requestId": null }
Realtime (Pusher channel resolution)
What this is for (plain English): Almost every real-time feature in this backend (consent status changes, care-context linking, health-info pushes, patient-share acknowledgements, NHCX callbacks, in-app notifications, etc.) is pushed to the frontend live over Pusher rather than the client having to poll. But a frontend can't just guess a Pusher channel name — it has to ask the server which channel(s) to subscribe to, using its own public key, and which event names to listen for. That's exactly what this module provides: a small "channel resolution" API layer sitting in front of Pusher itself.
Source: src/modules/realtime/ (realtime.routes.ts, .controller.ts, .service.ts, .schema.ts, .docs.ts, and realtime.events.ts — the canonical event-name catalog). Mounted at app.use('/v1/realtime', generalLimiter, realtimeRouter).
The general pattern: a client calls one of the "get channel" endpoints below once (typically on login/app-load), gets back a Pusher public key + one or more channel names + the full catalog of event names it might see on those channels (so the frontend never hard-codes an event string), and then opens its own Pusher WebSocket connection directly to Pusher's servers using that info — this API does not open the socket itself. If the client misses events (dropped connection, reconnect), it falls back to GET /v1/realtime/events to replay anything broadcast on that channel since a given timestamp, since Pusher itself never replays to a late/reconnecting subscriber.
There are two channel families:
phr-{abhaAddress}— one channel per patient (per ABHA address), carrying all patient-facing events (user-linking discovery/init/confirm, patient-share acknowledgement, notification creation).facility-{facilityId}— one channel per facility, carrying all operator-facing events (consent lifecycle, care-context linking, health-info pushes, subscriptions, NHCX claims, notifications).
Get PHR (Patient) Channels
GET /v1/realtime/channels — returns the Pusher channel(s) and event catalog a PHR patient app should subscribe to for one ABHA address.
- Who calls this: The PHR app, authenticated via Bearer token (
requireAuth). - Query params (
RealtimeChannelsQuerySchema):abhaAddress(required). - What happens next: Computes the per-ABHA channel name via
phrChannelForAbha(abhaAddress)and returns it alongside the Pusher public key and the fullREALTIME_EVENTScatalog. This is a pure lookup — no DB write, no socket opened.
Example: GET /v1/realtime/channels?abhaAddress=sharma2110@sbx
Example response (200):
{
"success": true,
"message": "Success",
"data": {
"abhaAddress": "sharma2110@sbx",
"publicKey": "a1b2c3d4e5f6a7b8c9d0",
"channels": {
"phr": "phr-sharma2110"
},
"events": {
"phr": {
"USER_LINKING_ON_DISCOVERED": "user-linking:on-discovered",
"USER_LINKING_ON_INIT": "user-linking:on-init",
"USER_LINKING_INIT": "user-linking:init",
"USER_LINKING_CONFIRMED": "user-linking:confirmed",
"USER_LINKING_ON_CONFIRMED": "user-linking:on-confirmed",
"PATIENT_SHARE_ON_SHARE": "patient-share:on-share",
"PHR_SELF_FETCH_GRANTED": "phr-self-fetch:granted",
"PHR_SELF_FETCH_SYNCING": "phr-self-fetch:syncing",
"NOTIFICATION_CREATED": "notification:created"
},
"facility": { "CONSENT_GRANTED": "consent:granted", "...": "..." },
"request": { "LINK_TOKEN_RECEIVED": "link-token:received", "...": "..." }
}
},
"requestId": "b8c3968b-cf3d-4fee-acae-0f173724a70e"
}
Get Facility Channel
GET /v1/realtime/facility-channel — the sibling of the above, but for the facility operator dashboard.
- Who calls this: Operator frontend (orvo-web/orvo-hub), authenticated via
X-Facility-IDheader (requireFacilityId) — deliberately not a Bearer token, since these callers authenticate via their own facility session, not an ABDM/PHR bearer. - What happens next: Returns
facility-{facilityId}as the channel name, the Pusher public key, cluster, and thefacilityslice ofREALTIME_EVENTS(consent, care-context, health-info, patient-share, subscription, notification, and NHCX event names). If nofacilityIdcan be resolved (missing header and no session), this does not error — it returns200withdata: nulland message"x-facility-id header required", so callers must check for a null payload rather than relying on an HTTP error status.
Example response when resolved (200):
{
"success": true,
"message": "Success",
"data": {
"channel": "facility-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"publicKey": "a1b2c3d4e5f6a7b8c9d0",
"cluster": "ap2",
"events": {
"CONSENT_GRANTED": "consent:granted",
"CONSENT_DENIED": "consent:denied",
"HEALTH_INFO_RECEIVED": "health-info:received",
"NHCX_CLAIM_UPDATED": "nhcx:claim-updated",
"NHCX_ELIGIBILITY_CHECKED": "nhcx:eligibility-checked",
"NHCX_PAYMENT_NOTIFIED": "nhcx:payment-notified",
"NHCX_PREAUTH_UPDATED": "nhcx:preauth-updated",
"...": "..."
}
},
"requestId": null
}
Example response when unresolved:
{ "success": true, "message": "x-facility-id header required", "data": null, "requestId": null }
Replay Missed Events
GET /v1/realtime/events — fetches durable copies of events broadcast on a specific channel, for a client that reconnected and might have missed live events in the gap.
- Who calls this: Either a PHR client (Bearer token) or the hub/operator dashboard (
X-HIP-IDheader) —requireAuthOrXHipIdaccepts either, specifically because the hub operator dashboard has no ABDM bearer token of its own and would otherwise 401 on every replay poll. - Query params (
RealtimeEventsQuerySchema):channel(required, exact channel name e.g.facility-fac_0002orphr-sharma2110),since(optional ISO timestamp; omit to get the most recent events). - What happens next: Queries the
RealtimeEventtable for rows on that channel withcreatedAt >= since(inclusive, so a boundary event is never skipped — the client dedupes byid), ordered oldest-first, capped at 100 rows. Events older than roughly one hour are periodically pruned from storage, sosinceshould stay recent (clients typically only ask for the last ~60 seconds).
Example: GET /v1/realtime/events?channel=facility-a1b2c3d4-e5f6-7890-abcd-ef1234567890&since=2026-07-18T09:00:00.000Z
Example response data:
[
{
"id": "evt_7f3a9c1d",
"event": "nhcx:claim-updated",
"payload": {
"correlationId": "b0c9d8e7-f6a5-5432-1098-765fedcba109",
"claimId": "d2b3c4d5-e6f7-4890-1234-56789abcdef0",
"decision": "APPROVED",
"status": "APPROVED",
"approvedAmount": 42000,
"remarks": null
},
"createdAt": "2026-07-18T09:20:00.000Z"
}
]
Logs
What this is for (plain English): Internal diagnostic tooling — a way for engineers/ops to inspect the application's own log stream, see which outbound calls to ABDM succeeded or failed, see how often each API route (including inbound ABDM callbacks) is actually being hit, and clean up old log data. This is not part of the ABDM protocol at all; it's operational tooling for running the service.
Source: src/modules/log/ (log.routes.ts, log.controller.ts, log.schema.ts, log.docs.ts, log.repo.ts). Mounted at app.use('/v1/logs', generalLimiter, logRouter).
Security note called out directly in the docs comments in this codebase: the two DELETE endpoints below are intended to be admin-only, but the routes are currently mounted with only the general rate limiter and no auth/role middleware — that restriction is not yet enforced in code. Treat this as a known gap, not an oversight to silently work around.
Query Logs
GET /v1/logs — searches/filters the application's own structured log entries.
- Who calls this: An internal ops/engineering tool (no ABDM party involved).
- Query params (
LogQuerySchema):level(error|warn|info|http|debug, optional),message(substring search, optional),before/after(ISO dates;afterdefaults to 7 days ago,beforedefaults to now),limit(default 50, max 200),offset(default 0). - What happens next: Queries the log store with those filters and returns a paginated result.
Example: GET /v1/logs?level=error&limit=20&after=2026-07-17T00:00:00Z
Example response data:
{
"logs": [
{
"id": "log_00931",
"level": "error",
"message": "[NHCX] payment callback failed",
"meta": { "err": "TypeError: Cannot read properties of undefined" },
"timestamp": "2026-07-18T02:15:00.000Z"
}
],
"pagination": { "total": 3, "limit": 20, "offset": 0, "hasMore": false }
}
Log Level Summary
GET /v1/logs/summary — a quick health-check count of log entries per level.
- Who calls this: Internal ops tooling / an admin health dashboard.
- What happens next: Groups log rows by level and returns counts.
Example response data:
{ "counts": { "error": 3, "warn": 12, "info": 204, "http": 891, "debug": 0 } }
Query Outbound API Calls
GET /v1/logs/api-calls — inspects our own outbound calls to ABDM (from the AbdmTransaction log table), for debugging failed gateway integrations.
- Who calls this: Internal ops/engineering.
- Query params (
ApiCallsQuerySchema):success(true|false, filters on the success flag recorded at call-time, not derived from HTTP status on read),method(GET|POST|PUT|PATCH|DELETE),action(case-insensitive substring match against an auto-derived action name likeinit/confirm/authenticate— not a fixed enum),before/after(ISO dates,afterdefaults to 7 days ago),limit(default 50, hard-capped 200),offset(default 0). - What happens next: Queries
AbdmTransactionwith those filters, paginated.
Example: GET /v1/logs/api-calls?success=false&action=init&limit=20
Example response data (abbreviated):
{
"apiCalls": [
{
"id": "txn_44231",
"url": "https://healthidsbx.abdm.gov.in/v0.5/consent-requests/init",
"method": "POST",
"action": "init",
"success": false,
"statusCode": 500,
"requestBody": { "...": "..." },
"responseBody": { "error": "Internal error" },
"createdAt": "2026-07-18T02:14:00.000Z"
}
],
"pagination": { "total": 1, "limit": 20, "offset": 0, "hasMore": false }
}
Endpoint Usage
GET /v1/logs/endpoint-usage — per-route hit counts across the whole API surface, including inbound ABDM callbacks — useful for spotting dead routes or usage spikes.
- Who calls this: Internal ops/engineering.
- Query params (
EndpointUsageQuerySchema):source(API|ABDM_CALLBACK, optional),search(substring match on method or route),limit(default 50, max 200),offset(default 0),sortBy(method|route|source|hitCount|lastStatusCode|firstSeenAt|lastSeenAt, defaulthitCount),sortOrder(asc|desc, defaultasc— so least-used routes surface first by default). - What happens next: Reads the
EndpointUsageStattable, which is populated as documented routes (registered in a*.docs.tsOpenAPI registry) and inbound callback routes are hit. A route with no row here has never been hit at all.
Example: GET /v1/logs/endpoint-usage?source=API&sortBy=hitCount&sortOrder=desc&limit=10
Example response data:
{
"routes": [
{
"id": "eus_991",
"source": "API",
"method": "POST",
"route": "/v1/nhcx/claim/submit",
"hitCount": 214,
"lastStatusCode": 202,
"firstSeenAt": "2026-02-01T10:00:00.000Z",
"lastSeenAt": "2026-07-18T09:12:00.000Z"
}
],
"pagination": { "total": 1, "limit": 10, "offset": 0, "hasMore": false }
}
Purge Old Logs
DELETE /v1/logs/purge?days=30 — hard-deletes log entries older than N days, for storage cleanup.
- Who calls this: Internal ops/admin. Caveat: not currently enforced as admin-only in code (see security note above) — anyone able to reach the route can call it.
- Query params (
PurgeQuerySchema):days(default 30, must be a positive integer). - What happens next: Hard-deletes matching rows from the log store; this is destructive and not reversible.
Example: DELETE /v1/logs/purge?days=30
Example response data: { "deleted": 812, "olderThanDays": 30 }
Purge All Logs
DELETE /v1/logs — hard-deletes every log entry regardless of age.
- Who calls this: Internal ops/admin. Same caveat as above — not currently gated by auth/role middleware.
- What happens next: Deletes all rows from the log store. No confirmation step, no body, irreversible.
Example response data: { "deleted": 4021 }