PHR App API Reference (M1 — Patient-Facing Surface)

What this covers

This document describes the "PHR App" surface of the orvo-abha backend: the API that powers a patient's own ABDM Health Records app experience — creating and logging into an ABHA (Ayushman Bharat Health Account), managing their profile, granting/revoking consent for hospitals to access their records, viewing their longitudinal health record (LHR) timeline, receiving notifications, managing app settings, and setting up a "health locker" (a service that automatically receives copies of the patient's records).

This is the "M1" (Milestone 1) actor known in ABDM terminology as PHR (Personal Health Record) — as opposed to the HIP (hospital/facility) side, the HPR (doctor registry) side, or NHCX (insurance claims), which are documented elsewhere. Every endpoint below is mounted under /v1/phr/... (a /v2/phr/... mirror exists for profile, consents, and lhr — same behavior, versioned path only, noted per-module below).

Almost everything here is a thin, credential-attaching proxy to ABDM's own gateway APIs — this backend's job is mostly to hold the patient's session token, RSA-encrypt sensitive fields (OTPs, passwords, mobile numbers) the way ABDM requires, and translate ABDM's raw responses into a consistent envelope. Every response (success or failure) is wrapped the same way:

{
  "success": true,
  "message": "Human-readable status message",
  "data": { "...": "endpoint-specific payload" },
  "requestId": "a1b2c3d4-...."
}

Two auth patterns recur throughout:

Rate limits: enrollment and login routes sit behind authLimiter (30 requests / 15 min per IP); everything else in this document sits behind generalLimiter (300 requests / 15 min per IP). Both are skipped entirely in local development.


Enrollment — creating a new ABHA

Plain English: this is PHR-app signup — turning an Aadhaar number, mobile number, or existing ABHA number into a usable ABHA account with a chosen "ABHA address" (like an email handle, e.g. rajesh.kumar@abdm, that identifies the patient across the whole ABDM network).

There are four distinct signup journeys, selected by enrollmentHint in the very first call, and (for abha-number) further split by otpMethod:

enrollmentHint Identifies via OTP channel Creates a new ABHA?
aadhaar 12-digit Aadhaar Aadhaar-linked mobile Yes — KYC-verified
abha-number + otpMethod: abdm Existing 14-digit ABHA number ABHA-registered mobile No — picks an existing address
abha-number + otpMethod: aadhaar Existing 14-digit ABHA number Aadhaar-linked mobile No — picks an existing address
mobile-number Any 10-digit mobile ABDM OTP system Yes — self-declared, no KYC

All routes are mounted at /v1/phr/enrollment behind authLimiter.

POST /v1/phr/enrollment/request

Step 1 of every journey — sends the enrollment OTP.

{
  "enrollmentHint": "aadhaar",
  "enrollmentValue": "234567890123",
  "otpMethod": "aadhaar"
}

For mobile-number: { "enrollmentHint": "mobile-number", "enrollmentValue": "9876543210" }. For abha-number: { "enrollmentHint": "abha-number", "enrollmentValue": "12345678901234", "otpMethod": "abdm" }.

{
  "success": true,
  "message": "OTP requested successfully",
  "data": {
    "txnId": "37d8d312-35a0-41e7-a6e4-107h6b18a5fa",
    "message": "OTP sent to mobile number ending with ******0903"
  },
  "requestId": "..."
}

POST /v1/phr/enrollment/verify

Step 2 — verifies the OTP (decrypted/encrypted server-side via RSA where ABDM requires it).

{
  "enrollmentHint": "aadhaar",
  "otpMethod": "aadhaar",
  "txnId": "37d8d312-35a0-41e7-a6e4-107h6b18a5fa",
  "otpValue": "123456",
  "mobile": "9876543210"
}

mobile is required only for aadhaar (the communication mobile for the new account) — the schema enforces this with a .refine().

{
  "success": true,
  "message": "Enrollment verified successfully",
  "data": {
    "txnId": "37d8d312-35a0-41e7-a6e4-107h6b18a5fa",
    "authResult": "success",
    "message": "ABHA created successfully",
    "tokens": {
      "token": "eyJhbGciOiJSUzUxMiJ9...",
      "expiresIn": 1800,
      "refreshToken": "eyJhbGciOiJSUzUxMiJ9...",
      "refreshExpiresIn": 1296000
    },
    "users": [
      {
        "abhaAddress": "rajesh.kumar@sbx",
        "abhaNumber": "91-3553-5100-0383",
        "fullName": "Rajesh Kumar",
        "kycStatus": "VERIFIED",
        "status": "ACTIVE"
      }
    ],
    "accounts": []
  },
  "requestId": "..."
}

POST /v1/phr/enrollment/mobile/request and /mobile/verify

Aadhaar-journey-only sub-flow: when the communication mobile given at verify-OTP differs from the Aadhaar-linked mobile, ABDM doesn't issue tokens immediately — this pair of endpoints verifies possession of that new mobile to finalize the account and obtain the session token.

POST /v1/phr/enrollment/address/suggestions

Returns candidate ABHA addresses (like a username-availability suggester).

{
  "txnId": "37d8d312-35a0-41e7-a6e4-107h6b18a5fa",
  "enrollmentHint": "mobile-number",
  "firstName": "Rajesh",
  "lastName": "Kumar",
  "dayOfBirth": "14",
  "monthOfBirth": "07",
  "yearOfBirth": "1990",
  "email": "rajesh.kumar@example.com"
}

For aadhaar, only txnId + enrollmentHint are needed — demographics already came from KYC.

{
  "success": true,
  "message": "ABHA address suggestions fetched successfully",
  "data": {
    "txnId": "23acf181-339d-4771-b532-5c5df4a28d19",
    "abhaAddressList": ["rajesh.kumar.2661997", "rajesh.kumar_1997"]
  },
  "requestId": "..."
}

GET /v1/phr/enrollment/pincode/:pincode

Resolves a 6-digit PIN code to state/district (used to auto-fill the address form).

GET /v1/phr/enrollment/address/availability

Checks whether a chosen ABHA address is still free, before final creation.

POST /v1/phr/enrollment/address

Final step — actually creates the ABHA address.

{
  "enrollmentHint": "mobile-number",
  "txnId": "23acf181-339d-4771-b532-5c5df4a28d19",
  "phrDetails": {
    "mobile": "9876543210",
    "firstName": "Rajesh",
    "lastName": "Kumar",
    "yearOfBirth": 1990,
    "dayOfBirth": 14,
    "monthOfBirth": 7,
    "gender": "M",
    "email": "rajesh.kumar@example.com",
    "address": "12 MG Road",
    "stateName": "Karnataka",
    "stateCode": 29,
    "districtName": "Bangalore Urban",
    "districtCode": 583,
    "pinCode": 560001,
    "abhaAddress": "rajesh.kumar@abdm",
    "password": "Str0ng!Pass"
  },
  "abhaAddress": "rajesh.kumar",
  "preferred": 1
}

For aadhaar, the body collapses to { "enrollmentHint": "aadhaar", "txnId": "...", "abhaAddress": "rajesh.kumar", "preferred": 1 } — ABDM already holds the demographics from KYC. mobile/password inside phrDetails are RSA-encrypted server-side before forwarding to ABDM.

POST /v1/phr/enrollment/email/verify

Optional post-creation step (Aadhaar journey) — sends an email verification link.

GET /v1/phr/enrollment/profile, /profile/qr, /profile/card

Fetch the enrollment-time profile / QR code / ABHA card for the account just created — distinct from the main /v1/phr/profile module (used post-login), these exist for the tail end of the signup flow before the patient necessarily logs in again.

Gotcha — the Aadhaar journey's token can't authenticate anything outside enrollment. Every client (web, iOS, Android) must handle this the same way. The tokens.token returned by /verify (and /mobile/verify) for the aadhaar hint is scoped by ABDM to the M1 account endpoints only (GET_ACCOUNT, what the /enrollment/profile routes above use) — it is not a general PHR-app session token. Sending it to anything gated by requirePhrAbhaAddress (/v1/phr/profile, /consents, /lhr, /patient-notifications, etc.) fails, because that middleware validates a token by calling ABDM's login-session GET /profile — a different, incompatible scope — and ABDM rejects the Aadhaar token outright. This is true whether the account is brand new or already existed; it's how ABDM scopes the token, not a bug in this service, so there's no backend fix, and no client can special-case it away either. It surfaces as the app appearing to log the patient in, then bouncing straight back to the login screen the instant it loads anything outside enrollment (e.g. a notifications badge).

Every client integrating this API must apply the same rule: never treat the Aadhaar enrollment token as a login session. Once the ABHA address is known (from /verify's users[0].abhaAddress/ABHAProfile, or from /address's response), immediately call POST /v1/phr/login/request/otp with { "loginHint": "abha-address", "loginId": "<address>" } to get a real session — one extra OTP round-trip, but the only path to a token that works outside the enrollment/M1 surface. orvo-phr's startAadhaarLoginHandoff (src/features/auth/store.ts) is one reference implementation of this handoff; the future iOS/Android clients need the equivalent. The mobile-number and abha-number journeys don't have this problem — their /address response returns a directly usable, login-scoped token.


Login — signing into an existing ABHA

Mounted at /v1/phr/login, behind authLimiter. Five loginHint values are supported, each with different OTP-channel and disambiguation semantics:

loginHint loginId format OTP sent to Needs verify/user?
aadhaar 12-digit Aadhaar Aadhaar-linked mobile If multiple ABHA accounts match
abha-number 14-digit ABHA number, no hyphens mobile linked to that number If multiple match
mobile 10-digit ABHA-linked mobile that mobile If multiple match
mobile-number 10-digit mobile (lookup key) that mobile Usually yes
abha-address full address, e.g. name@sbx mobile linked to it No — always direct

abha-address is the important special case: it is inherently unambiguous, so verify/otp returns final session tokens immediately. Calling verify/user for this hint fails with ABDM's misleadingly-worded "Invalid T-token" (ABDM-1006) — don't call it for this hint.

POST /v1/phr/login/request/otp

{ "loginHint": "abha-address", "loginId": "rajesh.kumar@sbx" }

or { "loginHint": "aadhaar", "loginId": "234567890123" }, { "loginHint": "mobile", "loginId": "9876543210" }, etc.

POST /v1/phr/login/verify/otp

{
  "success": true,
  "data": {
    "message": "Login successful",
    "authResult": "success",
    "tokens": {
      "token": "eyJhbGciOiJSUzUxMiJ9...",
      "expiresIn": 1800,
      "refreshToken": "eyJhbGciOiJSUzUxMiJ9...",
      "refreshExpiresIn": 1296000
    },
    "users": [
      {
        "abhaAddress": "rajesh.kumar@sbx",
        "fullName": "Rajesh Kumar",
        "abhaNumber": "91-3553-5100-0383",
        "status": "ACTIVE",
        "kycStatus": "VERIFIED"
      }
    ]
  },
  "requestId": "..."
}

Success response — multi-account match (e.g. mobile-number): same shape but tokens is absent, users has multiple entries, and txnId is present — the client must call verify/user next with the chosen abhaAddress.

POST /v1/phr/login/verify/user

Disambiguates between multiple ABHA accounts a mobile/Aadhaar/ABHA-number resolved to.

POST /v1/phr/login/refresh

POST /v1/phr/login/search

Looks up an ABHA address and its profile/auth-method metadata, ahead of choosing a login method.

{
  "success": true,
  "data": {
    "healthIdNumber": "91-3553-5100-0383",
    "abhaAddress": "rajesh.kumar@sbx",
    "authMethods": ["MOBILE_OTP", "AADHAAR_OTP"],
    "blockedAuthMethods": [],
    "status": "ACTIVE",
    "message": null,
    "fullName": "Rajesh Kumar",
    "mobile": "98765XXXXX"
  },
  "requestId": "..."
}

POST /v1/phr/login/verify

Login using ABHA address + password directly (no OTP).

{
  "success": true,
  "data": {
    "message": "Password verified successfully",
    "authResult": "success",
    "users": [
      {
        "abhaAddress": "hemant.bodhai_test@sbx",
        "fullName": "Hemant Bodhai",
        "abhaNumber": "91-5326-6278-1550",
        "status": "ACTIVE",
        "kycStatus": "VERIFIED"
      }
    ],
    "tokens": {
      "token": "...",
      "expiresIn": 1800,
      "refreshToken": "...",
      "refreshExpiresIn": 1296000
    }
  },
  "requestId": "..."
}

Profile — viewing and managing the ABHA account

Mounted at /v1/phr/profile (and mirrored at /v2/phr/profile) behind generalLimiter. All routes except /token/refresh require requireAuth (Bearer PHR session token); some also require requirePhrAbhaAddress (ABDM-verified ABHA address resolved from the token).

GET /v1/phr/profile

Full ABHA demographic profile for the current session.

{
  "success": true,
  "data": {
    "firstName": "Rajesh",
    "lastName": "Kumar",
    "dob": "14-07-1990",
    "gender": "M",
    "mobile": "9876543210",
    "email": "rajesh.kumar@example.com",
    "phrAddress": ["rajesh.kumar@sbx"],
    "address": "12 MG Road",
    "stateName": "Karnataka",
    "districtName": "Bangalore Urban",
    "pinCode": "560001",
    "ABHANumber": "91-3553-5100-0383",
    "abhaStatus": "ACTIVE"
  },
  "requestId": "..."
}

GET /v1/phr/profile/card and GET /v1/phr/profile/qr

Returns the ABHA card / QR code as a base64-encoded PNG.

GET /v1/phr/profile/switch

Lists other ABHA addresses linked to the current session, for a profile switch.

{
  "success": true,
  "data": {
    "txnId": "b1b6c3e7-5e2c-4c7f-9f02-9c8fbc3caa11",
    "users": [
      {
        "abhaAddress": "rajesh.kumar@sbx",
        "fullName": "Rajesh Kumar",
        "status": "ACTIVE",
        "kycStatus": "VERIFIED"
      }
    ],
    "tokens": { "token": "...", "expiresIn": 1800, "switchProfileEnabled": true }
  },
  "requestId": "..."
}

POST /v1/phr/profile/switch

POST /v1/phr/profile/logout

Care-context links (facility visits) for the patient, from ABDM's HIECM registry.

{
  "success": true,
  "data": {
    "Patient": {
      "id": "rajesh.kumar@sbx",
      "links": [
        {
          "hip": { "id": "IN0310000702_1", "name": "Orvo Test Hospital", "type": "HIP" },
          "referenceNumber": "PT-2026-0042",
          "display": "Rajesh Kumar",
          "hiType": "DiagnosticReport",
          "careContexts": [
            { "referenceNumber": "ENC-20260610-001", "display": "OPD Visit 10-Jun-2026" }
          ],
          "dateCreated": "2026-06-10T09:30:00.000Z"
        }
      ]
    }
  },
  "requestId": "..."
}

GET /v1/phr/profile/linked-hips

Merged view — the ABDM links above, joined with Orvo's own local sync metadata (last synced, resource type counts).

POST /v1/phr/profile/token/refresh

POST /v1/phr/profile/share

Patient scans a facility's QR code and shares their profile (Scan & Share flow).

{
  "intent": "PROFILE_SHARE",
  "metaData": {
    "hipId": "IN0310000702_1",
    "context": "counter-1",
    "hprId": null,
    "hfrId": null,
    "latitude": 28.6139,
    "longitude": 77.209
  },
  "profile": {
    "patient": {
      "abhaAddress": "rajesh.kumar@sbx",
      "abhaNumber": "91-3553-5100-0383",
      "name": "Rajesh Kumar",
      "gender": "M",
      "yearOfBirth": "1990",
      "monthOfBirth": "07",
      "dayOfBirth": "14",
      "phoneNumber": "9876543210",
      "address": {
        "line": "12 MG Road",
        "district": "Bangalore Urban",
        "state": "Karnataka",
        "pincode": "560001"
      }
    }
  }
}

Note: metaData.latitude/longitude accept either a string or a number and are coerced — the ABDM gateway→HIP callback sends them as strings while this outbound request sends numbers.

GET /v1/phr/profile/get-token-details

The patient's recent facility queue/counter tokens (OPD token numbers) — not auth tokens, despite the name.

{
  "success": true,
  "data": [
    {
      "id": 1042,
      "patientId": "rajesh.kumar@sbx",
      "tokenNumber": "A-014",
      "hipId": "IN0310000702_1",
      "hipName": "Orvo Test Hospital",
      "hipAddress": "12 MG Road, Bangalore",
      "expiresIn": "2026-07-18T18:00:00.000Z",
      "clientId": "orvo-app",
      "dateCreated": "2026-07-18T09:00:00.000Z",
      "counterCode": "OPD-1"
    }
  ],
  "requestId": "..."
}

Link ABHA number: POST /link/otp/request, POST /link/otp/verify, POST /link/confirm

A three-step flow to link a second ABHA number to the current session (e.g. linking a family member's or a previous ABHA number).

PATCH /v1/phr/profile

Full demographic profile update (name, DOB, gender, email, address) — not the login mobile number, which has its own OTP-gated flow below.

{
  "firstName": "Rajesh",
  "middleName": "",
  "lastName": "Kumar",
  "dayOfBirth": "14",
  "monthOfBirth": "07",
  "yearOfBirth": "1990",
  "gender": "M",
  "email": "rajesh.kumar@example.com",
  "address": "14 MG Road",
  "stateName": "Karnataka",
  "stateCode": "29",
  "districtName": "Bangalore Urban",
  "districtCode": "583",
  "pinCode": "560001"
}

Change password: POST /password/change

{
  "password": "N3wStr0ng!Pass"
}

The backend resolves the verified ABHA address from the authenticated session and RSA-encrypts the password before ABDM receives it.

Update mobile: POST /mobile/otp/request, POST /mobile/change

Unlike updateProfile, changing the login mobile requires OTP verification of the new number before ABDM associates it with the account.


Plain English: this is the screen where a hospital/HIU asks "can we see your medical records?" and the patient taps Approve or Deny. It also covers the patient's own self-fetch flow — pulling their own records into the PHR app from a hospital they've already visited — and letting the patient pre-authorize future requests from a HIU ("auto-approval") so they don't have to tap Approve every single time.

Mounted at /v1/phr/consents (mirrored at /v2/phr/consents), behind generalLimiter. Every route requires requireAuth; several also require requirePhrAbhaAddress.

GET /v1/phr/consents

Lists the patient's consent requests, proxying ABDM GET /api/hiecm/consent/v3/request.

GET /v1/phr/consents/{requestId}

POST /v1/phr/consents/{requestId}/approve

Grants a pending consent request.

{
  "consents": [
    {
      "hiTypes": ["DiagnosticReport", "OPConsultation"],
      "hip": { "id": "IN0310000702_1", "name": "Orvo Test Hospital", "type": "HIP" },
      "careContexts": [
        { "patientReference": "PT-2026-0042", "careContextReference": "ENC-20260610-001" }
      ],
      "permission": {
        "accessMode": "VIEW",
        "dateRange": { "from": "2025-01-01T00:00:00.000Z", "to": "2026-07-18T00:00:00.000Z" },
        "dataEraseAt": "2027-07-18T00:00:00.000Z",
        "frequency": { "unit": "DAY", "value": 1, "repeats": 1 }
      }
    }
  ]
}

POST /v1/phr/consents/{requestId}/deny

POST /v1/phr/consents/{requestId}/grant-self-fetch

Grants a patient self-fetch (PATRQT) request specifically — builds the care-context approve payload automatically from the patient's own linked records at the target HIP, since a self-fetch request is raised with hip: null / careContexts: null (ABDM requirement) and has nothing for the patient to manually pick.

POST /v1/phr/consents/revoke

Revokes one or more previously granted consent artefacts.

POST /v1/phr/consents/self-request

Patient-initiated pull of their own records from an external HIP they've already linked (they already know the care contexts — e.g. from /profile/links).

{
  "hipId": "IN0310000702_1",
  "hipName": "Orvo Test Hospital",
  "careContexts": [
    { "patientReference": "PT-2026-0042", "careContextReference": "ENC-20260610-001" }
  ],
  "hiTypes": ["DiagnosticReport", "Prescription"],
  "fromDate": "2025-01-01T00:00:00.000Z",
  "toDate": "2026-07-18T00:00:00.000Z",
  "autoApprove": true
}

hiTypes defaults to all 8 supported types if omitted. autoApprove: true (default) registers a PATRQT auto-approval policy first so the CM grants it automatically, mirroring the official ABHA app's behavior.

POST /v1/phr/consents/fetch-from-hip

Convenience wrapper around self-request — the caller only supplies hipId (+ optional date range/HI types); care contexts are auto-resolved from the patient's ABDM-linked records (GET /profile/links internally) rather than the caller having to supply them.

GET /v1/phr/consents/artifacts

Lists all granted consent artefacts.

GET /v1/phr/consents/artifacts/request/{requestId}

GET /v1/phr/consents/artifacts/{artifactId}

Auto-approval: POST /auto-approval, POST /auto-approval/{id}/enable, POST /auto-approval/{id}/disable

Lets the patient pre-authorize future consent requests from a specific HIU so they don't need to tap Approve every time.

{
  "hiu": { "id": "IN0310000702_1", "name": "Orvo Test Hospital", "type": "HIU" },
  "isApplicableForAllHIPs": true,
  "includedSources": [
    {
      "hiTypes": ["DiagnosticReport", "Prescription"],
      "purpose": {
        "text": "Self Requested",
        "code": "PATRQT",
        "refUri": "https://abdm.gov.in/consent/purpose/patrqt"
      },
      "hip": null,
      "period": { "from": "2025-07-18T00:00:00.000Z", "to": "2027-07-18T00:00:00.000Z" }
    }
  ]
}

Longitudinal Health Records (LHR) — the patient's unified record timeline

Plain English: once consent has been granted and a hospital's records have been pulled in via self-fetch, this is where the patient actually sees their health data — a searchable, filterable timeline of every document (lab report, prescription, discharge summary, etc.) ingested from every connected hospital.

Mounted at /v1/phr/lhr (mirrored at /v2/phr/lhr), behind generalLimiter. All routes require requireAuth + requirePhrAbhaAddress except /test-loopback (auth only, no ABHA address requirement — it's a diagnostic tool, not part of production flow).

GET /v1/phr/lhr/timeline

Paginated, reverse-chronological FHIR-resource timeline.

{
  "success": true,
  "data": {
    "data": [
      {
        "id": "clx1a2b3c0000qzrm8h6j9f2a",
        "resourceType": "DiagnosticReport",
        "resourceDate": "2025-06-10T09:30:00.000Z",
        "hipId": "IN0310000702_1",
        "careContextReference": "ENC-20250610-001",
        "hiType": "DiagnosticReport",
        "data": { "resourceType": "DiagnosticReport", "...": "raw FHIR JSON" },
        "createdAt": "2025-06-10T10:00:00.000Z"
      }
    ],
    "nextCursor": "clx1a2b3c0001qzrm8h6j9f2b",
    "total": 42
  },
  "requestId": "..."
}

cursor is an opaque keyset cursor (row id, ordered by resourceDate/id desc) — not a page offset, so pagination stays consistent even as new records arrive concurrently. total is the unfiltered count for the whole abhaAddress, not the filtered result count.

GET /v1/phr/lhr/sources

Lists distinct HIPs that have delivered data, with last-sync time and per-type counts.

{
  "success": true,
  "data": {
    "sources": [
      {
        "hipId": "IN0310000702_1",
        "hipName": "Orvo Test Hospital",
        "lastSyncedAt": "2026-07-10T10:00:00.000Z",
        "resourceTypes": [
          { "type": "DiagnosticReport", "count": 6 },
          { "type": "Prescription", "count": 3 }
        ]
      }
    ]
  },
  "requestId": "..."
}

GET /v1/phr/lhr/grouped

Up to 500 most recent records, grouped by originating HIP (no pagination/filtering — an overview, not deep history).

GET /v1/phr/lhr/self-fetch-status

Diagnostic-only endpoint for tracing why a patient's LHR is empty or incomplete — walks each recent PATRQT (self-fetch) consent through the pipeline stages (requested → granted → artefact received → health-info request raised → bundles received → resources stored) and reports exactly where it stalled.

GET /v1/phr/lhr/records/{id}

Single stored FHIR resource by its internal row id.

GET /v1/phr/lhr/records/{id}/bundle

The full FHIR document bundle (Composition + all referenced resources) the record belongs to.

POST /v1/phr/lhr/sync

Re-drives the self-fetch data-flow for a patient asynchronously.

POST /v1/phr/lhr/test-loopback

Test/diagnostic only — not part of the production ABDM flow. Simulates the HIP→HIU data push locally (encrypt→push→decrypt→normalize→store) for an already-granted self-fetch consent, because ABDM's sandbox doesn't reliably route data pushes back to a HIP that is also acting as its own HIU.


Notifications

There are two separate, non-overlapping notification surfaces for the PHR app — worth distinguishing clearly since they look similar but serve different purposes.

ABDM system notifications — GET /v1/phr/notifications

Plain English: these are notifications ABDM itself generates (e.g. "your consent request was granted", "a HIP's status changed") — a live read-through to ABDM, nothing is stored here.

Orvo-generated patient notifications — /v1/phr/patient-notifications

Plain English: these are notifications Orvo itself generates and stores locally (e.g. "your Scan & Share at Apollo Hospitals succeeded") — with actual read/unread tracking, which ABDM's own feed does not support.

Mounted behind generalLimiter; all routes require requireAuth + requirePhrAbhaAddress.

GET /v1/phr/patient-notifications

PATCH /v1/phr/patient-notifications/{id}/read

POST /v1/phr/patient-notifications/read-all


Settings — PHR app preferences

Plain English: a small local (not ABDM-synced) key-value preference store per patient — toggles like whether auto-subscription is on, whether manually-uploaded documents show up in the timeline, and whether vitals like heart rate/BMI are tracked on the dashboard.

Mounted at /v1/phr/settings behind generalLimiter; all routes require requireAuth + requirePhrAbhaAddress.

GET /v1/phr/settings

{
  "success": true,
  "data": {
    "abhaAddress": "rajesh.kumar@sbx",
    "settings": {
      "isSubscriptionEnabled": true,
      "manuallyUploaded": true,
      "heartRate": true,
      "bodyMassIndex": true,
      "subscriptionId": null
    },
    "createdAt": "2026-06-01T00:00:00.000Z",
    "updatedAt": "2026-06-01T00:00:00.000Z"
  },
  "requestId": "..."
}

Creates the row with defaults on first read (never 404s); missing keys are backfilled with defaults on every read, so a newly-added setting key still appears even for existing patients.

PATCH /v1/phr/settings

Partial merge update — only the 5 keys above are accepted (.strict() schema; unknown keys are rejected rather than silently stored).

POST /v1/phr/settings/reset

Replaces (not merges) the entire settings object with defaults.


Subscription — ongoing data-sharing arrangements

Plain English: where consent is a one-time (or scheduled) grant, a subscription is an ongoing arrangement where a hospital/HIU automatically receives new records as they're created — going forward, without a fresh consent request each time. This module is the patient-facing half of that; the mirror facility-side module (abdm/hip/hiu-subscription) is used by an operator dashboard to initiate these requests in the first place.

Mounted at /v1/phr/subscription (three sub-routers: /requests, /lockers, and the root), behind generalLimiter. All routes require requireAuth.

GET /v1/phr/subscription/requests

Lists pending/past subscription requests.

POST /v1/phr/subscription/requests/{requestId}/approve

{
  "isApplicableForAllHIPs": false,
  "includedSources": [
    {
      "hiTypes": ["Prescription", "DiagnosticReport"],
      "purpose": { "text": "Care Management", "code": "CAREMGT" },
      "hip": { "id": "IN0310000702_1", "name": "Orvo Test Hospital" },
      "categories": ["LINK", "DATA"],
      "period": { "from": "2026-07-14T00:00:00.000Z", "to": "2027-07-14T00:00:00.000Z" }
    }
  ],
  "excludedSources": [],
  "autoApprove": true,
  "hiuId": "IN0310000702_1",
  "hiuName": "Orvo Test Hospital",
  "hiTypes": ["Prescription", "DiagnosticReport"],
  "period": { "from": "2026-07-14T00:00:00.000Z", "to": "2027-07-14T00:00:00.000Z" }
}

autoApprove/hiuId/hiuName/hiTypes/period are stripped before forwarding to ABDM — they optionally set up an auto-approval policy for the granting HIU in the same action (best-effort; a failure there does not undo the grant).

POST /v1/phr/subscription/requests/{requestId}/deny

GET /v1/phr/subscription/requests/{requestId}

GET /v1/phr/subscription/lockers

Lists health lockers linked to the patient.

POST /v1/phr/subscription/lockers

Sets up a new (generic, ABDM-directory) health locker — for lockers other than Orvo's own (use /v1/phr/health-locker/setup below for the one-click Orvo case).

GET /v1/phr/subscription/lockers/{lockerId}

GET /v1/phr/subscription

Lists all of the patient's subscriptions (active and past).

GET /v1/phr/subscription/orvo

Finds the patient's subscription to Orvo specifically (our own system HIU), so the settings page can show a single enable/disable toggle without the frontend needing to know Orvo's HIU id.

GET /v1/phr/subscription/{subscriptionId}, PUT /v1/phr/subscription/{subscriptionId}, POST .../enable, POST .../disable


Health Locker — Orvo as a one-click health locker

Plain English: a "health locker" is a service that automatically receives copies of new health records as they're generated anywhere in the ABDM network — think of it as the patient's personal cloud folder for medical documents. This module is the one-click shortcut for making Orvo itself that locker (Orvo resolves its own locker id server-side, so the patient doesn't need to browse ABDM's locker directory), plus letting the patient manually upload a scanned document directly into it.

Mounted at /v1/phr/health-locker behind generalLimiter. All routes require requireAuth + requirePhrAbhaAddress.

POST /v1/phr/health-locker/setup

POST /v1/phr/health-locker/documents

Uploads a scanned document (PDF/image) directly into the patient's Orvo health locker — without needing a facility visit or a care-context reference already existing.

{
  "content": "JVBERi0xLjQKJcOkw7zDtsO...",
  "mimeType": "application/pdf",
  "documentType": "Report",
  "documentDate": "2026-06-01",
  "doctorName": "Dr. Suresh Menon",
  "facilityName": "Apollo Hospitals"
}

content is base64, capped at ~14 MiB encoded (≈10 MB source file). mimeType must be one of application/pdf, image/jpeg, image/png. documentType maps internally to a FHIR HI-type (Report → DIAGNOSTICREPORT, Other → HEALTHDOCUMENTRECORD, etc.).