If you are a CTO or engineering lead at an Indian hospital chain, a digital health startup, or a clinical SaaS, the question of building an ABDM Health Information User (HIU) integration eventually lands on your desk. The brief sounds simple — "let our doctors pull a patient's longitudinal record from the ABDM network." The implementation is anything but.
This article is the reference architecture we wish existed when we built our first HIU. It is stack-neutral — implement in .NET, Node, Java, or Go. It covers the full M3 flow end to end: consent request, patient grant, data fetch, Fidelius decryption, FHIR rendering, storage, and compliance with the dataEraseAt obligation. Every section is opinionated based on production lessons. No marketing fluff.
Why HIU is harder than it looks
HIU is the consumer side of healthcare interoperability in India. The Ayushman Bharat Digital Mission routes consent and data between four actors that you mostly do not control: the patient's ABHA app, the ABDM Gateway, your HIU, and one or more HIPs — Health Information Providers like hospitals, labs, pharmacies that hold the source data. Your code coordinates four asynchronous lifecycles simultaneously: token refresh, consent state, data-fetch transaction state, and FHIR bundle decryption.
Three concepts trip up teams new to ABDM:
- Consent is a separate object from the data. A consent artifact is a signed JSON describing what the patient agreed to share — which care contexts, which HI types, which date range, how long the data may be stored. It is fetched once after the patient grants in their ABHA app, and it contains the one-time encryption keypair the HIP must use. The actual FHIR bundles arrive later, encrypted with that key.
- Every push is end-to-end encrypted with Fidelius. ABDM never sees the cleartext data. You generate an ECDH keypair per consent, send the public key in the data request, the HIP uses it to encrypt with AES-256-GCM, and only your HIU can decrypt. Implementing Fidelius incorrectly is the single most common cause of failed certifications.
- dataEraseAt is a legal obligation. Every consent carries an expiry timestamp after which the HIU must purge or anonymise the fetched data. Skipping this is a regulatory breach, not an engineering choice.
The rest of this article is the architecture that handles all three correctly.
Mental model: the M3 flow
Read this diagram before you write any code. The numbered steps are the canonical M3 — consent plus data fetch — sequence. Every implementation maps to these twelve messages, no matter what framework you build it in.
Six things to internalise:
- Consent artifact — signed JSON containing care contexts the patient approved, HI types, date range, plus a one-time keypair for encryption. Fetched once after grant. Cached for the consent's lifetime.
- Care context (CC) — a reference like
IM-21orINV-8881. One CC equals one clinical encounter at the HIP. The HIP owns the namespace. - HI types — the eight categories of clinical data ABDM supports: OPConsultation, Prescription, DiagnosticReport, DischargeSummary, ImmunizationRecord, WellnessRecord, HealthDocumentRecord, Invoice. Patient picks which to share at grant time.
- Date range — patient picks a window (last 6 months, last year, etc.). Data with effectiveDateTime outside the window may not be returned.
- Fidelius — ABDM's mandated E2EE: ECDH on Curve25519 plus AES-256-GCM. HIU generates a fresh keypair per consent; HIPs encrypt with it; only the HIU can decrypt.
- dataEraseAt — the expiry datetime after which the HIU must purge the data. Non-negotiable.
The six operator screens
HIU is fundamentally an operator-facing product — a doctor or front-desk staff fires consent requests and reads back patient records. The minimum-viable HIU has six screens. Build them in any framework; their logical structure is what matters.
Screen A — Consent Request
A form the operator fills after collecting the patient's ABHA address. Fields: ABHA address (text), purpose code (dropdown — usually CAREMGT), date range (from + to), HI types (multi-select), HIP filter (optional, blank for pan-India), expiry, dataEraseAt. Validation: ABHA address matches the standard pattern (letters, digits, dot, underscore — 3 to 18 chars before @, 2 to 32 after) or 14-digit number; from <= to <= today; at least one HI type; dataEraseAt > expiry. On submit, your backend calls ABDM's /consent-requests/init and writes a row to your local consent_requests table with status PENDING.
Screen B — Requests Dashboard
Table view of all consent requests with status badges (PENDING / REQUESTED / GRANTED / DENIED / REVOKED / EXPIRED). Per-row actions: cancel a pending request, fetch data on a granted one, view detail on any row. Filter by status and ABHA address. Auto-poll every 30 seconds for terminal-state changes.
Screen C — Data Fetch in Progress
Once an operator clicks "Fetch now" on a granted consent, show a six-step visual checklist: artifact fetched, data request sent, awaiting HIP push, bundle received, decrypted, stored. Use long-polling or SSE to update state — naive 5-second polling works for MVP. Timeout after 5 minutes with a clear retry path.
Screen D — Records Viewer
Left rail lists care contexts grouped by date, badged by HI type. Right panel renders the decoded FHIR bundle. Each HI type renders differently: OPConsultation has Chief Complaints / Diagnosis / Vitals / Medications / Document sections; Prescription is a Medications table plus PDF; DiagnosticReport shows test results plus the lab report PDF; ImmunizationRecord lists shots (vaccine, given, mfg, lot) plus a Vaccination Certificate PDF. Add a "Show raw FHIR" toggle for power users.
Screen E — Consent Detail
Drill-down per consent: timeline of state changes, list of linked care contexts with per-CC fetch status, the raw signed artifact (collapsible), and a Revoke action that calls ABDM's /consents/revoke.
Screen F — Settings / Admin
Credentials (client ID and secret, encrypted at rest), HIU ID and facility ID, X-CM-ID environment toggle (sbx or prod), registered callback URLs (read-only — what ABDM has on file), default dataEraseAt window. Add a health check page that probes the gateway, validates your callback URLs are reachable from the public internet, and confirms Fidelius is loaded.
Gateway APIs: the five calls your HIU makes
All against https://dev.abdm.gov.in/api/hiecm (sandbox) or the production base URL. Every request needs these headers:
Authorization: Bearer {accessToken}
REQUEST-ID: {fresh uuid}
TIMESTAMP: {ISO 8601 UTC, e.g. 2026-05-17T15:30:00.000Z}
X-CM-ID: sbx (or prod)
X-HIU-ID: {your registered HIU ID}
Content-Type: application/json 1. Get access token
POST /gateway/v3/sessions
{
"clientId": "YOUR_CLIENT_ID",
"clientSecret": "YOUR_CLIENT_SECRET",
"grantType": "client_credentials"
} Returns { accessToken, tokenType, expiresIn }. Cache the token and refresh ~30 seconds before expiresIn lapse. The token is short-lived (typically 1200s).
2. Initiate consent request
POST /consent-requests/init
{
"consent": {
"purpose": { "text": "Care Management", "code": "CAREMGT",
"refUri": "http://projecteka.in/consent-purpose" },
"patient": { "id": "patient@sbx" },
"hiu": { "id": "YOUR_HIU_ID" },
"requester": { "name": "Dr. Operator Name",
"identifier": { "type": "REGNO", "value": "MH123456",
"system": "https://www.mciindia.org" } },
"hiTypes": ["OPConsultation","Prescription","DiagnosticReport",
"DischargeSummary","ImmunizationRecord","WellnessRecord",
"HealthDocumentRecord","Invoice"],
"permission": {
"accessMode": "VIEW",
"dateRange": { "from": "2025-11-17T00:00:00.000Z",
"to": "2026-05-17T23:59:59.000Z" },
"dataEraseAt": "2026-11-17T23:59:59.000Z",
"frequency": { "unit": "HOUR", "value": 1, "repeats": 0 }
}
}
} Returns 202 Accepted. Store the REQUEST-ID header you sent — ABDM references it in all subsequent callbacks.
3. Fetch consent artifact (after GRANTED notification arrives)
POST /consents/fetch
{ "consentId": "{consentId-from-grant-notification}" } Returns 202. ABDM then calls your /consents/on-fetch callback with the full artifact, including the care contexts the patient approved.
4. Request health data
POST /health-information/cm/request
{
"hiRequest": {
"consent": { "id": "{consentId}" },
"dateRange": { "from": "...", "to": "..." },
"dataPushUrl": "https://your-hiu.example.com/health-information/transfer",
"keyMaterial": {
"cryptoAlg": "ECDH",
"curve": "Curve25519",
"dhPublicKey": {
"expiry": "2026-05-18T15:30:00.000Z",
"parameters": "Curve25519/32byte random key",
"keyValue": "{base64-encoded HIU public key}"
},
"nonce": "{base64-encoded 32-byte random nonce}"
}
}
} Returns 202. ABDM acknowledges via /health-information/hiu/on-request, then HIPs push encrypted bundles directly to your dataPushUrl.
5. Revoke consent (HIU-initiated)
POST /consents/revoke
{ "consents": [{ "id": "{consentId}" }] } Patient-initiated revoke comes through the /consents/hiu/notify callback below. HIU-initiated revoke is rarer but supported.
Callbacks: the six endpoints your HIU exposes
ABDM is event-driven. Your HIU must publicly expose six HTTPS callback endpoints and register them with ABDM during onboarding. Every callback must ACK with 200 OK within five seconds — fail-soft and queue the heavy work async. Self-signed certificates are rejected; production needs a real LB certificate.
- POST /consent-requests/on-init — Ack of your
/initcall. Update local row to status REQUESTED. - POST /consent-requests/on-status — Status changes for cancel and similar. Update local row.
- POST /consents/hiu/notify — The most important callback. Patient took action in their ABHA app: GRANTED, DENIED, REVOKED, or EXPIRED. On GRANTED, trigger
/consents/fetch. Always ACK back to/consents/hiu/on-notifywith the requestId you received. - POST /consents/on-fetch — Response to your
/consents/fetch. Contains the full consent artifact includingconsentDetail.careContexts[](what HIPs will push),consentDetail.permission, and a detached JWS signature you should verify against ABDM's published JWKS in production. - POST /health-information/hiu/on-request — Ack of your data request. Contains the
transactionIdthat HIPs will reference in their pushes. - POST {your dataPushUrl} — The actual encrypted bundle delivery. Body contains
entries[]each with base64content,media: "application/fhir+json", an md5checksum, and thecareContextReference. Body also contains the HIP'skeyMaterial(their public key + nonce) which you need to decrypt. After processing, POST status back to/health-information/notifywith TRANSFERRED, FAILED, or PARTIAL.
Fidelius: encryption that often gets implemented wrong
Fidelius is the NHA-mandated end-to-end encryption scheme. It is the single most common reason ABDM certifications fail. Get it right once and the rest of the integration becomes mechanical.
Per-consent setup
- HIU generates a fresh ECDH keypair on Curve25519 using libsodium, tweetnacl, BouncyCastle, or @stablelib/x25519 — pick what matches your stack.
- HIU generates a random 32-byte nonce.
- HIU embeds the public key plus nonce in
/health-information/cm/requestunderkeyMaterial.dhPublicKey.keyValueandkeyMaterial.nonce. - HIU stores the private key plus nonce against the
consentIdin a write-once-read-many table. Encrypt the private key at rest using KMS or your app secret store.
Decryption when bundles arrive
For each encrypted entry pushed to your dataPushUrl:
- Derive the shared secret:
ECDH(your_private_key, hip_public_key_from_keyMaterial). - XOR the HIU nonce with the HIP nonce. The XOR result is your AES-GCM IV (use the first 12 bytes).
- Derive the AES-256-GCM key from the shared secret via HKDF-SHA256, using the XOR'd nonce as salt.
- AES-256-GCM decrypt the base64-decoded ciphertext.
- UTF-8 decode the plaintext into a FHIR JSON string.
The single biggest pitfall is public key serialisation. ABDM uses raw 32-byte X25519 keys, base64-encoded. Some crypto libraries wrap keys in SubjectPublicKeyInfo (DER) by default — strip that 12-byte prefix before passing to ECDH. If your decryption returns garbage, this is the first thing to check.
Verification
Before shipping, write a round-trip unit test: generate HIU and HIP keypairs, encrypt a known FHIR bundle with the HIP's perspective, decrypt with the HIU's perspective, assert byte-equality. Cross-test against the published reference implementations to catch IV-derivation bugs early.
NRCES FHIR profiles: each HI type has different slicing rules
The decoded bundles conform to NRCES profiles at https://nrces.in/ndhm/fhir/r4/. Each HI type has a different Composition.section slicing rule. Get this wrong on the renderer side and PDFs will silently fail to appear.
Two profile rules surprise everyone:
- PrescriptionRecord has a closed slice that allows MedicationRequest and Binary only. DocumentReference is rejected by validators. The auto-generated prescription PDF must go in as a
Binaryresource, not wrapped in DocumentReference. - ImmunizationRecord has the opposite rule: closed slice with Immunization, ImmunizationRecommendation, and DocumentReference. Binary is not allowed here. The vaccination certificate PDF must be a DocumentReference.
Most other HI types (OPConsultation, DischargeSummary, WellnessRecord, Invoice) accept DocumentReference for the auto-generated PDF. DiagnosticReportRecord has a closed slice of at most two entries: DiagnosticReport (0..1) plus DocumentReference (0..1).
On the renderer, build one helper that walks both groups.DocumentReference AND groups.Binary looking for application/pdf attachments, and call it from every per-HI-type render function. The same code surfaces every kind of attached document regardless of which profile rule applies.
Patient resources carry both identifiers — the 14-digit ABHA Number under system https://healthid.abdm.gov.in AND the @cm-style ABHA Address under https://healthid.ndhm.gov.in. The PHR app reads Patient.identifier to render the document header; emit both or the patient banner shows only the number.
Storage schema: five tables, zero ORM magic required
The minimum storage footprint is five tables. SQL Server, PostgreSQL, MySQL — they all work. Schema below is portable SQL with light type aliases.
CREATE TABLE consent_requests (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
request_id VARCHAR(64) UNIQUE NOT NULL,
consent_id VARCHAR(64),
abha_address VARCHAR(255) NOT NULL,
purpose_code VARCHAR(32),
hi_types TEXT,
date_from DATETIME,
date_to DATETIME,
data_erase_at DATETIME,
status VARCHAR(32),
hiu_keypair_id BIGINT,
requested_at DATETIME DEFAULT CURRENT_TIMESTAMP,
granted_at DATETIME,
revoked_at DATETIME,
raw_artifact LONGTEXT
);
CREATE TABLE hiu_keys (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
consent_id VARCHAR(64),
private_key VARBINARY(64),
public_key VARBINARY(64),
nonce VARBINARY(64),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME
);
CREATE TABLE data_flow_requests (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
consent_id VARCHAR(64) NOT NULL,
transaction_id VARCHAR(64) UNIQUE,
status VARCHAR(32),
requested_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME,
expected_cc_count INT,
received_cc_count INT DEFAULT 0,
error_message TEXT
);
CREATE TABLE clinical_bundles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
consent_id VARCHAR(64) NOT NULL,
transaction_id VARCHAR(64),
care_context_reference VARCHAR(128) NOT NULL,
hi_type VARCHAR(64),
bundle_json LONGTEXT,
received_at DATETIME DEFAULT CURRENT_TIMESTAMP,
checksum_match BIT,
decode_status VARCHAR(32),
UNIQUE KEY uniq_cc (consent_id, care_context_reference)
);
CREATE TABLE consent_audit (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
request_id VARCHAR(64),
consent_id VARCHAR(64),
event_type VARCHAR(64),
event_source VARCHAR(32),
payload_json LONGTEXT,
at DATETIME DEFAULT CURRENT_TIMESTAMP
); Add a daily job that scans consent_requests for rows where data_erase_at < NOW() and either NULLs the corresponding clinical_bundles.bundle_json or deletes the rows entirely, per your organisation's compliance policy. This job is not optional. Schedule it day one — retrofitting compliance is painful.
State machine: every consent goes through these transitions
Six states. Each transition must be logged to consent_audit with timestamp and source. State transitions are durable and idempotent — if ABDM redelivers the same notification (it sometimes does), the second processing must be a no-op.
PENDING→REQUESTEDon/consent-requests/on-initackREQUESTED→GRANTED/DENIED/REVOKEDon/consents/hiu/notifyGRANTED→EXPIREDwhenpermission.dataEraseAt < NOW()- Any non-terminal state →
CANCELLEDif HIU calls/consent-requests/cancel
A pattern that pays off: do not auto-trigger data fetch on GRANTED. Show the GRANTED status in the operator dashboard and let the operator click "Fetch now". This gives them an out for "I asked for the wrong patient" scenarios and avoids races where multiple GRANTED notifications arrive in quick succession.
Twelve gotchas already paid for
This is the section that will save weeks of debugging. Every item below is a real failure mode we have hit in production HIU integrations.
- ABDM throttle — repeating an
/initcall for the same patient within 5–10 minutes returnsABDM-9999 Duplicate request. Build in a backoff; do not keep firing. - X-CM-ID is mandatory —
sbxfor sandbox,prodfor production. Omitting it gets you cryptic 4xx errors. - REQUEST-ID must be a fresh UUID per request — reusing one triggers dedup rejection at the gateway.
- TIMESTAMP clock skew — more than 60 seconds drift between your server and ABDM gateway = 401. Sync NTP on every host.
- dataPushUrl must be publicly reachable HTTPS — ABDM verifies the certificate. Self-signed = rejected. Use ngrok with a reserved domain in dev, real LB cert in prod.
- Bundle pagination — HIPs may split large bundles across multiple
pageNumberpushes. Wait forpageNumber == pageCountbefore marking the transaction TRANSFERRED. - status="not-done" Immunization entries violate FHIR R4 constraint
imm-1without astatusReason. Either skip them at the HIP push or surface them as such — never emit them bare. - PDF location varies by profile — Prescription requires Binary in the section; Immunization requires DocumentReference; others allow either. Your renderer must check both
groups.DocumentReferenceANDgroups.Binaryforapplication/pdfattachments. - Never store clientSecret in plain text — KMS or app secret manager. Rotate periodically.
- Do not auto-fetch on GRANTED — surface the granted consent in the dashboard, let the operator click. Better UX, fewer race conditions.
- Verify the artifact's JWS signature — ABDM's
signaturefield is a detached JWS. Verify against ABDM's published JWKS for production. Sandbox you can skip but log a warning. - dataEraseAt enforcement is the HIU's legal responsibility — non-compliance is a regulatory breach, not an engineering bug. Daily purge job, no exceptions.
Polling and async — bridging callback timing with operator UX
ABDM is event-driven via callbacks. Your operator UI is impatient and synchronous. Bridge the gap with one of three approaches:
- Long-polling — frontend hits a backend endpoint that blocks up to 60 seconds waiting for state change. Returns immediately if state has already changed. Simplest for MVP.
- SSE (Server-Sent Events) — backend pushes state changes per consent to subscribed clients. Lighter than WebSocket, HTTP-only, works through corporate firewalls.
- Naive polling every 5 seconds — wasteful but acceptable for low-traffic operator screens. Most HIUs in the wild use this.
The actual ABDM callback processing must ACK within five seconds, but you can decouple ACK from heavy work. Write a small inbox table, persist the callback body, ACK ABDM, then a worker process picks it up to decrypt and store.
Onboarding checklist
For teams starting from zero, the path to production is:
- Register your HIU at the ABDM registration portal. Receive sandbox
clientIdandclientSecret. - Register your six callback URLs in the ABDM bridge config. ABDM does a connectivity ping — all six must respond green.
- Run a smoke-test consent flow against a known-working sandbox ABHA address (a few are publicly documented for testing). Fire init, grant in the ABHA app, watch notify land, fetch artifact, request data, decrypt, view.
- Run the NHA HIU certification test suite via CodeDecodeLabs. Categories: HIU_INIT_GRANT_*, HIU_REQ_DATA_*, HIU_FLOW_302_*.
- Apply for production credentials.
- Schedule the dataEraseAt purge job before going live. Verify with a synthetic past-expiry row.
For most teams the build is 6–10 weeks; certification 2–4 weeks; production access another 2–4 weeks. Most of the schedule risk lives in Fidelius implementation and NHA cert iteration.
What we have learned shipping this in production
Three observations from running HIU integrations for hospital clients in India:
Encryption is the entire game. Once Fidelius round-trips cleanly against test vectors, the rest of the build is mechanical CRUD plus state-machine transitions. Teams that get encryption right in week one finish 4–6 weeks faster than teams that defer it.
NRCES profile validation is non-negotiable. NHA cert harness rejects bundles that violate slicing rules. The renderer side has to handle both DocumentReference and Binary attachments universally — one helper function shared across all per-HI-type render paths is the cleanest pattern.
Operator UX dictates adoption. Two hospitals on the same codebase will have wildly different adoption depending on whether the operator screens are 6 clicks or 16 clicks to fetch a patient's record. Spend disproportionate time on screens A, B, and D — they are the daily-use surfaces.
If you are evaluating whether to build, partner, or buy your ABDM HIU layer, we have published the reference architectures (.NET and Node/TS) as private starter codebases for our integration clients. Our team has shipped both HIU and HIP sides for hospitals, pharma networks, and PHR products since the M2 sandbox era.
Need help architecting your HIU? Our ABDM integration services cover end-to-end M1, M2, and M3 buildouts, certification support, and production handover. We also work on broader healthcare interoperability solutions spanning FHIR, HL7, and EHR connections beyond the ABDM ecosystem. Talk to our team to scope your build.



