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:
Bearer <token>— the ABDM-issued PHR session token (a JWT ABDM calls a "T-token" or "X-token" depending on the flow stage), obtained from login or enrollment and forwarded as-is to ABDM on every authenticated call. This backend does not verify the JWT signature itself (requireAuthonly checks a token is present) — ABDM is the actual authority, and any call that needs to trust the identity inside the token (not just "some session exists") additionally round-trips to ABDM's ownGET /profileto confirm the token is genuinely ABDM-issued (requirePhrAbhaAddressmiddleware).- No auth — the early enrollment/login steps (OTP request/verify) are necessarily anonymous, since the patient doesn't have a session yet.
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.
- Auth: none.
- Request body (discriminated by
enrollmentHint):
{
"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 response:
{
"success": true,
"message": "OTP requested successfully",
"data": {
"txnId": "37d8d312-35a0-41e7-a6e4-107h6b18a5fa",
"message": "OTP sent to mobile number ending with ******0903"
},
"requestId": "..."
}
- Errors: 400 if the identifier is malformed for the given hint (e.g. a 10-digit string
sent as
aadhaar); 422 on schema validation failure; 429 if the sameenrollmentHint:enrollmentValuepair requested an OTP in the last 60 seconds (assertOtpResendAllowed). - Triggers: the patient taps "Create ABHA" and enters their Aadhaar/mobile/ABHA number in the signup screen.
POST /v1/phr/enrollment/verify
Step 2 — verifies the OTP (decrypted/encrypted server-side via RSA where ABDM requires it).
- Auth: none.
- Request body:
{
"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 response (Aadhaar journey — creates the account and returns a session):
{
"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": "..."
}
- Errors: 400 invalid OTP/txnId; 422 validation.
- Triggers: patient submits the 6-digit OTP they received.
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.
- Auth: none.
- Request body (
/mobile/request):{ "txnId": "37d8d312-...", "mobile": "9876543210" } - Request body (
/mobile/verify):{ "txnId": "37d8d312-...", "otpValue": "123456" } - Success response (both):
{ "txnId": "...", "message": "OTP sent to mobile number ending with ******0903" } - Triggers: only reached when the Aadhaar-linked mobile and the mobile the patient wants to use for the account differ.
POST /v1/phr/enrollment/address/suggestions
Returns candidate ABHA addresses (like a username-availability suggester).
- Auth: none.
- Request body:
{
"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 response:
{
"success": true,
"message": "ABHA address suggestions fetched successfully",
"data": {
"txnId": "23acf181-339d-4771-b532-5c5df4a28d19",
"abhaAddressList": ["rajesh.kumar.2661997", "rajesh.kumar_1997"]
},
"requestId": "..."
}
- Triggers: patient reaches the "choose your ABHA address" screen.
GET /v1/phr/enrollment/pincode/:pincode
Resolves a 6-digit PIN code to state/district (used to auto-fill the address form).
- Auth: none.
- Path param:
pincode— must match^\d{6}$, e.g.560001. - Success response:
{ "success": true, "data": { "stateName": "Karnataka", "stateCode": 29, "districtName": "Bangalore Urban", "districtCode": 583 }, ... }(shape fromenrollmentService.resolvePincode). - Triggers: patient types their PIN code in the mobile-number/abha-number signup form.
GET /v1/phr/enrollment/address/availability
Checks whether a chosen ABHA address is still free, before final creation.
- Auth: none.
- Query:
?abhaAddress=rajesh.kumar&enrollmentHint=mobile-number - Success response:
{ "success": true, "data": { "status": true }, ... }(true= available). - Errors: 422 if the address is shorter than the minimum for that
enrollmentHint(8 chars formobile-number, 4 forabha-number/aadhaar). - Triggers: live-typing check as the patient edits their chosen address.
POST /v1/phr/enrollment/address
Final step — actually creates the ABHA address.
- Auth: none (except Aadhaar journey, where the account token from
/verifyis sent asAuthorization: Bearer <token>and read manually in the controller — not gated byrequireAuthsince other hints have no session yet at this point). - Request body (mobile-number/abha-number journeys — full demographic profile):
{
"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.
- Success response:
{ "success": true, "data": { "txnId": "...", "abhaAddress": "rajesh.kumar@abdm" }, ... } - Errors: 422 validation (including the address-length
.refine()). - Triggers: patient taps "Confirm" after picking their ABHA address and (for mobile/abha-number journeys) setting a password.
POST /v1/phr/enrollment/email/verify
Optional post-creation step (Aadhaar journey) — sends an email verification link.
- Auth:
requireAuth(Bearer = the account token from Create-ABHA/Mobile-Update verify). - Request body:
{ "email": "rajesh.kumar@example.com" } - Success response:
{ "success": true, "data": { "message": "Email verification link sent successfully" }, ... } - Errors: 401 missing/invalid token; 422 invalid email format.
- Triggers: patient adds an email address to their new account and asks to verify it.
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.
- Auth:
requireAuth. - Query:
?enrollmentHint=aadhaar(identifies which journey created the account). - Success response:
{ "success": true, "data": { ... } }— profile/QR/card shape depends on ABDM's raw passthrough. - Errors: 401 unauthorized.
- Triggers: the "Welcome" screen right after account creation, showing the patient their new ABHA card.
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
- Auth: none.
- Request body:
{ "loginHint": "abha-address", "loginId": "rajesh.kumar@sbx" }
or { "loginHint": "aadhaar", "loginId": "234567890123" }, { "loginHint": "mobile", "loginId": "9876543210" }, etc.
- Success response:
{ "success": true, "data": { "txnId": "b1b6c3e7-5e2c-4c7f-9f02-9c8fbc3caa11", "message": "OTP sent successfully" }, ... } - Errors: 400 invalid
loginHint/loginIdcombination; 422 validation; 429 resend cooldown. - Triggers: patient enters their identifier on the login screen and taps "Send OTP".
POST /v1/phr/login/verify/otp
- Auth: none.
- Request body:
{ "loginHint": "abha-address", "txnId": "b1b6c3e7-...", "otp": "123456" } - Success response — single/unambiguous match (e.g.
abha-address):
{
"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.
- Errors: 400 invalid OTP/txnId; 422 validation.
- Triggers: patient submits the OTP they received.
POST /v1/phr/login/verify/user
Disambiguates between multiple ABHA accounts a mobile/Aadhaar/ABHA-number resolved to.
- Auth:
requireAuth— Bearer = the T-token returned byverify/otp(not the final session token). - Request body:
{ "abhaAddress": "rajesh.kumar@sbx", "txnId": "b1b6c3e7-5e2c-4c7f-9f02-9c8fbc3caa11" } - Success response: final
JwtTokenBundle—{ "success": true, "data": { "token": "...", "expiresIn": 1800, "refreshToken": "...", "refreshExpiresIn": 1296000 }, ... } - Errors: 401 unauthorized; 422 validation; ABDM rejects this call with "Invalid T-token"
(ABDM-1006) if the login hint was
abha-address(nothing to disambiguate there). - Triggers: patient picks one ABHA account from a list of several linked to the same mobile/Aadhaar/ABHA number.
POST /v1/phr/login/refresh
- Auth: none (the refresh token itself is the credential).
- Request body:
{ "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } - Success response:
JwtTokenBundle(same shape as above). - Errors: 422 validation.
- Triggers: the client's access token expired (~30 min) and it silently refreshes in the background.
POST /v1/phr/login/search
Looks up an ABHA address and its profile/auth-method metadata, ahead of choosing a login method.
- Auth: none.
- Request body:
{ "abhaAddress": "rajesh.kumar@sbx" } - Success response:
{
"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": "..."
}
- Errors: 400 not found/invalid; 422 validation.
- Triggers: patient types an ABHA address and the app wants to show which login methods (OTP/password) are available for it.
POST /v1/phr/login/verify
Login using ABHA address + password directly (no OTP).
- Auth: none.
- Request body:
{ "abhaAddress": "rajesh.kumar@sbx", "password": "Str0ng!Pass" }— the password is RSA-encrypted server-side before being sent to ABDM. - Success response:
{
"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": "..."
}
- Errors: 400 invalid address or wrong password; 422 validation.
- Triggers: patient chooses "log in with password" instead of OTP.
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.
- Auth:
requireAuth. - Success response: an
ABHAProfile-shaped object:
{
"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": "..."
}
- Errors: 401 unauthorized.
- Triggers: the PHR app's "My Profile" screen loads.
GET /v1/phr/profile/card and GET /v1/phr/profile/qr
Returns the ABHA card / QR code as a base64-encoded PNG.
- Auth:
requireAuth. - Success response:
{ "success": true, "data": { "data": "iVBORw0KGgoAAAANSUhEUgA..." }, ... } - Errors: 401 unauthorized.
- Triggers: patient taps "Show my ABHA card/QR" to display or share it at a facility.
GET /v1/phr/profile/switch
Lists other ABHA addresses linked to the current session, for a profile switch.
- Auth:
requireAuth. - Success response:
{
"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": "..."
}
- Triggers: patient has multiple ABHA addresses (e.g. one for a family member managed under the same login) and opens the profile switcher.
POST /v1/phr/profile/switch
- Auth:
requireAuth. - Request body:
{ "txnId": "b1b6c3e7-...", "abhaAddress": "family.member@sbx", "tToken": "eyJhbGci..." } - Success response:
JwtTokenBundle— new session tokens for the switched-to profile. - Triggers: patient picks a different ABHA profile from the switcher list.
POST /v1/phr/profile/logout
- Auth:
requireAuth. - Success response:
{ "success": true, "data": { "message": "Logged out" }, ... } - Triggers: patient taps "Logout".
GET /v1/phr/profile/links
Care-context links (facility visits) for the patient, from ABDM's HIECM registry.
- Auth:
requireAuth. - Query:
?offset=0&limit=20(optional). - Success response:
{
"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": "..."
}
- Triggers: used internally as the data source for consent self-fetch flows, and shown directly as a "linked facilities" list in the app.
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).
- Auth:
requireAuth,requirePhrAbhaAddress. - Success response:
{ "success": true, "data": { ... merged HIP+sync-state list ... } } - Triggers: "Connected Facilities" screen showing sync freshness per facility.
POST /v1/phr/profile/token/refresh
- Auth: none (validated by the refresh token itself).
- Request body:
{ "refreshToken": "eyJhbGci..." } - Success response:
{ "success": true, "data": { "tokens": { "token": "...", "expiresIn": 1800, "refreshToken": "...", "refreshExpiresIn": 1296000 } } } - Triggers: access token expiring.
POST /v1/phr/profile/share
Patient scans a facility's QR code and shares their profile (Scan & Share flow).
- Auth:
requireAuth. The requester HIU comes from the PHR HIU saved in ABDM settings, which must be registered under the PHR client. A suppliedX-HIU-IDmust match that setting (422 otherwise). Missing or cleared configuration blocks initiation with 409; there is no fallback to the platform self-fetch HIU. - Request body:
{
"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.
- Success response: raw ABDM acknowledgement passthrough, e.g.
{ "success": true, "data": { "requestId": "...", "status": "ACKNOWLEDGED" } }. - Errors: 401 unauthorized.
- Triggers: patient scans a QR code displayed at a hospital reception/counter.
GET /v1/phr/profile/get-token-details
The patient's recent facility queue/counter tokens (OPD token numbers) — not auth tokens, despite the name.
- Auth:
requireAuth. - Query:
?limit=10 - Success response:
{
"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": "..."
}
- Triggers: patient checks their recent OPD queue number after a Scan & Share at a counter.
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).
- Auth:
requireAuthfor all three. /link/otp/requestbody:{ "authMethod": "abha", "abhaNumber": "12-3456-7890-1234" }(authMethodis"abha"for OTP-to-linked-mobile, or"aadhaar"for Aadhaar OTP). Response:OtpRequestResponse—{ "txnId": "...", "message": "OTP sent successfully" }./link/otp/verifybody:{ "authMethod": "abha", "txnId": "...", "otpValue": "123456" }Response:{ "txnId": "...", "token": "...", "refreshToken": "..." }./link/confirmbody:{ "transactionId": "b1b6c3e7-..." }(same txn id from the request step). Response: raw ABDM confirmation object.- Errors: 401 unauthorized on all three.
- Triggers: patient goes to "Link another ABHA number" in account settings, enters the ABHA number, verifies OTP, and confirms.
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.
- Auth:
requireAuth. - Request body:
{
"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"
}
- Success response:
{ "success": true, "message": "Profile updated", "data": { ... } } - Errors: 400 bad request; 401 unauthorized; 422 validation.
- Triggers: patient edits their profile in "Edit Profile" and saves.
Change password: POST /password/change
- Auth:
requireAuth. /password/changebody:
{
"password": "N3wStr0ng!Pass"
}
The backend resolves the verified ABHA address from the authenticated session and RSA-encrypts the password before ABDM receives it.
- Success response:
{ "success": true, "message": "Your password is successfully changed", "data": { ... } } - Errors: 401 unauthorized; 422 validation (weak password - 8+ chars, upper, lower, digit, symbol, no keyboard runs like "qwe"/"123").
- Triggers: "Change Password" in account settings.
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.
- Auth:
requireAuthfor both. /mobile/otp/requestbody:{ "mobile": "9123456780" }— OTP goes to this new number, not the one on file./mobile/changebody:{ "txnId": "b1b6c3e7-...", "otpValue": "123456" }- Success response (change):
{ "success": true, "message": "Mobile number updated successfully", "data": { ... } } - Errors: 401 unauthorized; 422 validation; 429 resend cooldown on request.
- Triggers: patient updates their registered mobile number in account settings.
Consent — granting, denying, and revoking access to health records
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.
- Auth:
requireAuth. - Query:
?limit=20&offset=0&status=REQUESTED(limitrequired 1-100;statusone ofREQUESTED,EXPIRED,DENIED,GRANTED,REVOKED,ALL). - Success response:
{ "success": true, "data": { "requests": [ { "id": "c2a6e6e0-...", "status": "REQUESTED", "hiu": { "id": "IN0310000702_1", "name": "Orvo Test Hospital" }, "purpose": { "text": "Care Management", "code": "CAREMGT" }, "createdAt": "2026-07-10T09:00:00.000Z" } ], "size": 1 }, ... } - Note: if ABDM's underlying response has no real requests it returns a
{error:{...}}placeholder entry (ABDM-1001 "No data found") instead of an empty array — this backend normalizes that case torequests: []. - Triggers: patient opens the "Consent Requests" tab in the app.
GET /v1/phr/consents/{requestId}
- Auth:
requireAuth. - Path param:
requestId(UUID). - Success response: full consent request detail — HIU, purpose, HI types, date range, permission.
- Triggers: patient taps into a specific pending request to review it before deciding.
POST /v1/phr/consents/{requestId}/approve
Grants a pending consent request.
- Auth:
requireAuth. - Request body:
{
"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 }
}
}
]
}
- Success response:
{ "success": true, "message": "Consent request approved", "data": { ... } } - Triggers: patient reviews a consent request detail and taps "Approve".
POST /v1/phr/consents/{requestId}/deny
- Auth:
requireAuth. - Request body:
{ "reason": "Not my current treating hospital" }(optional/nullable). - Success response:
{ "success": true, "message": "Consent request denied", "data": { ... } } - Triggers: patient taps "Deny" on a consent request.
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.
- Auth:
requireAuth,requirePhrAbhaAddress. - Request body: none.
- Success response:
{ "success": true, "message": "Self-fetch consent granted", "data": { ... } } - Errors: 409 if the consent isn't in
REQUESTEDstatus anymore; 422 if noexternalHipwas recorded locally for this request; 404 if the HIP is linked but has no care contexts to grant. - Triggers: internal/automatic step of the self-fetch flow below, or a manual "Grant" tap if auto-approval wasn't used.
POST /v1/phr/consents/revoke
Revokes one or more previously granted consent artefacts.
- Auth:
requireAuth. - Request body:
{ "consents": ["a1f3c9e2-1234-4a5b-9c6d-7e8f9a0b1c2d"] } - Success response:
{ "success": true, "message": "Consent revoked", "data": { ... } } - Triggers: patient taps "Revoke access" on a previously granted consent.
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).
- Auth:
requireAuth,requirePhrAbhaAddress. - Request body:
{
"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.
- Success response:
{ "success": true, "message": "Self-consent request created", "data": { "consentId": "...", "status": "REQUESTED", "initiationStatus": "..." }, ... }(HTTP 201). - Errors: 400 if the target HIP is the same registered entity as this system's own HIU (ABDM rejects same-entity HIP/HIU consents).
- Triggers: patient taps "Fetch my records from this hospital" for a facility whose care contexts they already know.
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.
- Auth:
requireAuth,requirePhrAbhaAddress. - Request body:
{ "hipId": "IN0310000702_1", "hipName": "Orvo Test Hospital", "hiTypes": ["DiagnosticReport"], "fromDate": "2025-01-01T00:00:00.000Z", "toDate": "2026-07-18T00:00:00.000Z", "autoApprove": true } - Success response:
{ "success": true, "message": "Self-consent request created", "data": { "consentId": "...", "status": "REQUESTED", "careContextCount": 3 }, ... }(HTTP 201). Internally, once created, this backend auto-approves the consent on the patient's behalf shortly after (ABDM's own auto-approval policy is unreliable in the sandbox), so the data flow triggers without further patient action. - Errors: 404 if the patient has no linked care contexts at that
hipId, or the HIP is linked but has zero care contexts; 400 ifhipIdis the same entity as this system's HIU. - Triggers: patient taps "Fetch records" on a facility card in their "Connected Facilities" list, without needing to know the underlying care-context references.
GET /v1/phr/consents/artifacts
Lists all granted consent artefacts.
- Auth:
requireAuth. - Query:
?limit=20&offset=0&status=GRANTED - Success response:
{ "success": true, "data": { "artefacts": [ { "id": "a1f3c9e2-...", "status": "GRANTED", "hip": {...}, "hiu": {...} } ] } } - Triggers: "My Consents" list view showing active data-sharing grants.
GET /v1/phr/consents/artifacts/request/{requestId}
- Auth:
requireAuth. - Success response: artefacts tied to one consent request.
- Errors: 404 if the consent request isn't in
GRANTEDstatus. - Triggers: drilling into a specific approved request to see its resulting artefact(s).
GET /v1/phr/consents/artifacts/{artifactId}
- Auth:
requireAuth. - Success response: full artefact detail — care contexts, HI types, signature.
- Triggers: viewing the full detail/signature of one granted artefact.
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.
- Auth:
requireAuthfor all. /auto-approvalbody:
{
"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" }
}
]
}
/auto-approval/{autoApprovalId}/enableand/disable: no body, path param only.- Success responses:
{ "success": true, "message": "Auto-approval policy created" | "enabled" | "disabled", "data": { ... } } - Triggers: patient turns on "Always approve requests from this hospital" in a facility's settings, or later pauses/resumes it.
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.
- Auth:
requireAuth,requirePhrAbhaAddress. - Query:
?abhaAddress=rajesh.kumar@sbx&resourceType=DiagnosticReport&hipId=IN0310000702_1&fromDate=2025-01-01T00:00:00%2B05:30&toDate=2026-01-01T00:00:00%2B05:30&limit=20&cursor=clx1a2b3c0000qzrm8h6j9f2a(all filters optional exceptabhaAddressandlimit, which defaults to 20). - Success response:
{
"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.
- Triggers: patient opens "My Health Records" and scrolls the timeline, or applies a filter.
GET /v1/phr/lhr/sources
Lists distinct HIPs that have delivered data, with last-sync time and per-type counts.
- Auth:
requireAuth,requirePhrAbhaAddress. - Query:
?abhaAddress=rajesh.kumar@sbx - Success response:
{
"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": "..."
}
- Triggers: "Connected Sources" summary view.
GET /v1/phr/lhr/grouped
Up to 500 most recent records, grouped by originating HIP (no pagination/filtering — an overview, not deep history).
- Auth:
requireAuth,requirePhrAbhaAddress. - Query:
?abhaAddress=rajesh.kumar@sbx - Success response:
{ "success": true, "data": { "groups": [ { "hipId": "IN0310000702_1", "hipName": "Orvo Test Hospital", "lastSyncedAt": "...", "totalRecords": 9, "records": [ /* FhirRecordEntry[] */ ] } ] } } - Triggers: the app's default "Records by Hospital" landing view.
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.
- Auth:
requireAuth,requirePhrAbhaAddress. - Query:
?hipId=IN0310000702_1(optional, narrows to one facility). - Success response: a rich diagnostic object including
consents[](per-consent pipeline state, including the rawsentConsentPayload— for self-fetch this must showhip: null, careContexts: nullor ABDM silently drops the request),recentInboundHits,healthInfoPushShapes, and decrypt-failure diagnostics. - Triggers: support/engineering troubleshooting when a patient reports "my records aren't showing up" — not part of the normal patient-facing flow.
GET /v1/phr/lhr/records/{id}
Single stored FHIR resource by its internal row id.
- Auth:
requireAuth,requirePhrAbhaAddress. - Path param:
id(FHIR resource row ID). Query:?abhaAddress=rajesh.kumar@sbx(required, must match the owner). - Success response:
{ "success": true, "data": { "id": "...", "resourceType": "DiagnosticReport", "resourceDate": "...", "hipId": "IN0310000702_1", "careContextReference": "ENC-...", "data": { ...fhir... }, "createdAt": "..." } } - Errors: 400 missing
abhaAddress; 404 not found, or found but owned by a different patient (returns 404 rather than leaking existence, to avoid an IDOR). - Triggers: patient taps into a single record from the timeline.
GET /v1/phr/lhr/records/{id}/bundle
The full FHIR document bundle (Composition + all referenced resources) the record belongs to.
- Auth:
requireAuth,requirePhrAbhaAddress. - Path param/query: same as above.
- Success response:
{ "success": true, "data": { "bundle": { "resourceType": "Bundle", "entry": [ ... ] } } }— synthesizes a single-resource collection bundle if the record predates bundle linkage. - Errors: 400/404 same as above.
- Triggers: patient opens the "full document" view (e.g. rendering a complete discharge summary rather than just its summary card).
POST /v1/phr/lhr/sync
Re-drives the self-fetch data-flow for a patient asynchronously.
- Auth:
requireAuth,requirePhrAbhaAddress. - Request body:
{ "abhaAddress": "rajesh.kumar@sbx", "consentId": "c2a6e6e0-6e2f-4b9b-9f0a-1a2b3c4d5e6f" }(consentIdoptional — defaults to the firstGRANTEDconsent, used only to pick which facility gets a realtime "sync triggered" event). - Success response: HTTP 202,
{ "success": true, "data": { "status": "queued" }, ... }(does not wait for the HIP to deliver data). - Triggers: patient taps "Refresh" / "Sync now" on the records screen.
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.
- Auth:
requireAuthonly (norequirePhrAbhaAddress). - Request body:
{ "consentId": "c2a6e6e0-6e2f-4b9b-9f0a-1a2b3c4d5e6f" } - Success response: HTTP 202.
- Errors: 400 if
consentIdis missing, the consent/artefact isn't found, or no prior health-information request exists for it. - Triggers: engineering/QA use only, when testing end-to-end record ingestion against the ABDM sandbox.
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.
- Auth:
requireAuth. - Query:
?limit=20&offset=0&fromDate=2026-06-01T00:00:00.000Z(all optional). - Success response: raw ABDM passthrough — shape and pagination fully controlled by ABDM, not documented locally (may change without an Orvo deploy).
- Errors: 400 ABDM rejected malformed
fromDate/pagination; 401 unauthorized; 422 limit/offset out of range. - Triggers: the app's notification bell polling ABDM directly for system-level events.
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
- Query:
?limit=20&offset=0&unreadOnly=true(all optional;limitdefaults to 50). - Success response:
{ "success": true, "data": { "notifications": [ { "id": "ntf_4b2a1c9d", "title": "Profile shared successfully", "body": "Shared with Orvo Test Hospital", "isRead": false, "createdAt": "2026-07-18T09:00:00.000Z" } ], "total": 12, "unreadCount": 3 }, ... } - Errors: 401 unauthorized / ABHA address unresolvable.
- Triggers: patient opens the in-app notification bell.
PATCH /v1/phr/patient-notifications/{id}/read
- Path param:
id, e.g.ntf_4b2a1c9d. - Success response:
{ "success": true, "data": null, ... }— always 200, even for an unknown/foreign id (the update is scoped byabhaAddress+idtogether viaupdateMany, which never reports "0 rows matched" back to the caller as a 404). - Triggers: patient taps a single notification to open/dismiss it.
POST /v1/phr/patient-notifications/read-all
- Request body: none.
- Success response:
{ "success": true, "data": null, ... } - Triggers: patient taps "Mark all as read".
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 response:
{
"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.
- Triggers: the app's Settings screen loads.
PATCH /v1/phr/settings
Partial merge update — only the 5 keys above are accepted (.strict() schema; unknown keys
are rejected rather than silently stored).
- Request body:
{ "settings": { "isSubscriptionEnabled": false, "heartRate": true } }(at least one key required). - Success response:
{ "success": true, "message": "Settings updated", "data": { "abhaAddress": "rajesh.kumar@sbx", "settings": { ... merged ... }, "updatedAt": "..." } } - Errors: 422 unknown key, wrong type, or empty
settingsobject. - Triggers: patient flips a toggle in Settings.
POST /v1/phr/settings/reset
Replaces (not merges) the entire settings object with defaults.
- Request body: none.
- Success response:
{ "success": true, "message": "Settings reset to defaults", "data": { "abhaAddress": "rajesh.kumar@sbx", "settings": { "isSubscriptionEnabled": true, "manuallyUploaded": true, "heartRate": true, "bodyMassIndex": true, "subscriptionId": null }, "updatedAt": "..." } } - Triggers: patient taps "Reset to defaults" in Settings.
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.
- Query:
?subscriptionLimit=20&subscriptionOffset=0&consentLimit=20&consentOffset=0&status=REQUESTED - Success response:
{ "success": true, "data": { "requests": [ ... ], "size": 1 } } - Triggers: patient opens "Subscription Requests" list.
POST /v1/phr/subscription/requests/{requestId}/approve
- Request body:
{
"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).
- Success response:
{ "success": true, "message": "Subscription request approved", "data": { ... } } - Triggers: patient approves a hospital's request for an ongoing data feed.
POST /v1/phr/subscription/requests/{requestId}/deny
- Request body:
{ "reason": "Not currently my treating hospital" }(optional). - Success response:
{ "success": true, "message": "Subscription request denied", "data": { ... } } - Triggers: patient denies the subscription request.
GET /v1/phr/subscription/requests/{requestId}
- Success response: full detail of one subscription request.
- Triggers: viewing a request before deciding.
GET /v1/phr/subscription/lockers
Lists health lockers linked to the patient.
- Success response:
{ "success": true, "data": { "lockers": [ { "id": "...", "name": "Orvo", "status": "ACTIVE" } ] } } - Triggers: "My Health Lockers" list.
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).
- Auth:
requireAuth,requirePhrAbhaAddress(needed to attach the patient's ABHA address to a pending SubscriptionRequest row if the chosen locker happens to be Orvo's). - Request body:
{ "lockerId": "some-third-party-locker-id", "lockerName": "Third Party Locker", "autoApprove": true } - Success response:
{ "success": true, "message": "Locker setup completed", "data": { ... } } - Errors: 400 if
lockerIdis missing. - Triggers: patient picks a locker service from ABDM's directory and taps "Enable".
GET /v1/phr/subscription/lockers/{lockerId}
- Success response: locker detail including status and linked HIU.
- Triggers: viewing a specific locker's configuration.
GET /v1/phr/subscription
Lists all of the patient's subscriptions (active and past).
- Success response:
{ "success": true, "data": { "subscriptions": [ ... ] } } - Triggers: "My Subscriptions" overview screen.
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.
- Success response:
{ "success": true, "data": { "subscriptionId": "sub_123", "status": "GRANTED" } }or{ "success": true, "data": null }if none exists yet. - Triggers: the "Auto-sync with Orvo" toggle on the settings screen reads its state.
GET /v1/phr/subscription/{subscriptionId}, PUT /v1/phr/subscription/{subscriptionId}, POST .../enable, POST .../disable
- Auth:
requireAuthfor all. - PUT body: arbitrary partial config object (HI types/categories to change), forwarded as-is to ABDM.
- Success responses:
{ "success": true, "message": "Subscription fetched" | "updated" | "enabled" | "disabled", "data": { ... } } - Triggers: patient views/edits a subscription's scope, or pauses/resumes it.
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
- Request body:
{ "autoApprove": true }(optional — sets up a blanket auto-approval policy for Orvo, covering all mandated HI types over a 1-year-back/1-year-forward window, in the same call). - Success response: raw ABDM locker-subscription passthrough, e.g.
{ "success": true, "message": "Orvo health locker set up", "data": { "subscriptionId": "sub_456", "status": "REQUESTED" } } - Errors: 401 unauthorized; 500 if Orvo isn't configured as a system HIU/locker in ABDM Settings (a server misconfiguration, not something the patient can fix by retrying).
- Triggers: patient taps "Use Orvo as my Health Locker" on onboarding or in settings.
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.
- Request body:
{
"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.).
- Success response:
{ "success": true, "message": "Document uploaded", "data": { "careContextReference": "locker-upload-...", "status": "PROCESSED" } }(HTTP 201). - Errors: 422 wrong mime type / oversized file / missing required fields.
- Triggers: patient taps "Upload a document" and picks/scans a PDF or photo of a paper report to add to their own record set.