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:

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.

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.

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.

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.

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.

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.

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/status and /v1/on_status are the one pair of header tables in the whole spec that never list x-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 (CoverageEligibilityOnCheckSchema etc.) 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 (see nhcx.service.ts and the fhir/ 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 the x-hcx-* protected-header names directly at the top level, unlike every other flow's {"payload": "<JWE>"} envelope. handleErrorNotification/NhcxErrorNotificationSchema implement 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.

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.

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.

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.

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.

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.

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.

Example request body:

{
  "dataClassification": {
    "public": ["OPConsultation"],
    "internal": ["DischargeSummary", "Prescription"],
    "confidential": ["WellnessRecord"]
  }
}

Example response (200):

{ "success": true, "message": "Success", "data": { "updated": true }, "requestId": null }

PATCH /v1/settings/consent-defaults — sets the default consent expiry window and approval behavior used when pre-filling consent-init requests for a facility.

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.

{ "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.

PUT /v1/admin/feature-flags

Creates or toggles one (key, app, environment) row.

{ "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.


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.

GET /v1/reports/consent-metrics — approval rate and consent volume breakdown by status.

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.

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.

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).

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.

Example response data:

{
  "topHIUs": [
    { "name": "IN0310000702_1", "accessCount": 37 },
    { "name": "insurer.star-health@nhcx", "accessCount": 12 }
  ]
}

GET /v1/reports/daily-trends — day-by-day counts of consents created, care-context links, and data-access requests, for a trend chart.

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.

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.

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.

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").

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:

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.

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.

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.

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.

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.

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.

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.

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.

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.

Example response data: { "deleted": 4021 }