ABDM M2 Certification in 2026: HIP Test Cases, Callbacks, FHIR and Fidelius Fixes That Unblock Dev Teams
Software Development Expert
Writes about software development, scalable architecture, and practical problem-solving across modern digital products. Focuses on turning complex technical ideas into clear, real-world solutions.

ABDM M2 certification proves that your software works as a Health Information Provider (HIP): it links each patient visit to their ABHA, answers discovery, linking and consent callbacks from the ABDM gateway, and pushes the patient's records as encrypted FHIR bundles when a consent is granted. It is where most teams lose months, because M2 is not an API form-fill like M1. It is a distributed system with asynchronous callbacks, a FHIR data pipeline and a custom encryption protocol, all tested case by case by NHA.
This guide shows what NHA tests for M2, the five technical walls that stop development teams, and the exact fix for each: HIP-initiated linking, callback handling, FHIR bundles, Fidelius encryption and the certification checklist. It is written from our own delivery work taking hospital software through M2, with working code. Updated September 2026.
What ABDM M2 is, and where it sits among the milestones
ABDM has three integration milestones. M1 is ABHA creation and verification at the registration desk. M2 makes your system a HIP that shares the records it holds. M3 makes it a Health Information User (HIU) that requests records from others. To exit the sandbox you need any two milestones, and for a hospital system M1 plus M2 is the usual pair. Our ABDM integration step-by-step guide covers the full sequence from sandbox login to go-live.
If you want M2 built or reviewed against NHA's test cases rather than learned the hard way, our ABDM integration services team does exactly that for HMIS vendors and hospitals.
What NHA tests in M2 certification
NHA's M2 test sheet checks six areas. Each is a set of test cases that must pass without manual workarounds.
| Area | What NHA checks | Where teams fail |
|---|---|---|
| Health record creation | Valid FHIR R4 bundles for each health information type, NRCeS profiles, ABHA in the patient identifier | Missing fields that show as N/A in the PHR app |
| HIP-initiated linking | Link token by mobile or Aadhaar OTP, token received on your webhook, records visible in the PHR app, SMS deep link | Name not matching the ABHA record exactly |
| User-initiated linking | Facility found in PHR search, discovery returns care contexts, OTP on link init, confirmation on link confirm | Discovery answered late or without the request ID |
| Consent | Granted consent stored and acknowledged fast, revoked consent deleted, expired consent marked | No acknowledgement, or data kept after revocation |
| Sharing records | Request acknowledged, bundles built and encrypted, pushed, TRANSFERRED notified, within the time window | Fidelius errors, tunnel down at push time |
| Scan and Share, opt-out | Profile shared by QR with a token issued; ABHA deactivation removes the link and keeps clinical data | Deleting clinical data on opt-out |
ABDM M2 documentation: where the specifications live
The official M2 documentation is on the ABDM sandbox developer portal, and the FHIR profiles are published by NRCeS in the ABDM FHIR implementation guide. Two practical additions help: our public ABDM V3 Postman collection has every M2 call ready to run, and the ABDM FHIR bundle examples repository has working bundles. Use the NRCeS profiles as the source of truth for bundle structure; sample bundles found elsewhere often carry errors.
The ABDM technology stack behind M2: gateway and protocols
Every M2 exchange goes through the ABDM gateway, which fronts the health information exchange and consent manager (HIE-CM). The protocols are the same across all milestones:
- REST over HTTPS to the gateway, with a session token obtained with your client ID and secret.
- Asynchronous callbacks: most calls return 202 and the answer arrives later at your registered bridge URL; you reply on the matching
on-*API. - Headers that carry identity and correlation:
REQUEST-ID,TIMESTAMP,X-CM-ID, andX-HIP-IDfor M2 (M3 usesX-HIU-ID). - FHIR R4 documents built to NRCeS profiles for the records themselves.
- Fidelius, an ECDH plus AES-GCM scheme, to encrypt records end to end between provider and requester.
The five walls that stop ABDM M2 development
Across the M2 projects we have worked on, the time goes to the same five problems, and none of them is "calling the API".
Wall 1: FHIR bundles from relational databases
ABDM wants each record as a FHIR R4 document Bundle with a Composition first, followed by the Patient, Practitioner, Organization, Encounter and clinical resources it references, each declaring its NRCeS profile. Hospital systems store the same visit as rows across OPD, prescription, vitals and diagnosis tables, so the work is a mapping layer: internal codes to SNOMED CT where the profile expects it, every reference wired with urn:uuid: identifiers, and every section coded correctly for the record type. Miss one reference and the bundle fails validation; leave out a field the PHR app displays and the tester sees N/A. Our guide to ABDM FHIR N/A fields and NRCeS compliance fixes lists the fields that cause most failures.
The health information types you link and share are OPConsultation, Prescription, DiagnosticReport, DischargeSummary, ImmunizationRecord, WellnessRecord, HealthDocumentRecord and Invoice. Build the ones your facility actually produces first, starting with the outpatient consultation.
Wall 2: Fidelius encryption
Fidelius is not standard X25519. It uses Curve25519 in the short Weierstrass form that BouncyCastle implements, public keys encoded as X.509 SubjectPublicKeyInfo, and a key derivation where the two parties' nonces are XORed: the first 20 bytes become the HKDF salt and the last 12 bytes the AES-GCM IV. Use a library that accepts custom curve parameters; the Web Crypto API and Node's built-in X25519 produce a different shared secret and the receiver simply cannot decrypt. The full walkthrough is in our Fidelius encryption Node.js guide, and the code is further down this page.
p: 7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed
a: 2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA984914A144
b: 7B425ED097B425ED097B425ED097B425ED097B425ED097B4260B5E9C7710C864
n: 1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed Wall 3: asynchronous callbacks
In V3 every M2 interaction is request in, 202 out, answer back later. Three rules decide whether testing passes:
- Return 202 before doing anything else. A handler that queries the database first times out, and a 500 fails the test case.
- Echo the request ID. The body of your
on-*reply must carry the incoming request ID so ABDM can match it. - Assume callbacks can be lost. A consent notification that never arrives is not resent. Keep the tunnel or endpoint healthy and monitor it.
Wall 4: care context linking
Linking runs in two directions. In user-initiated linking the patient searches for your facility in a PHR app, you answer discovery, send an OTP on link init and confirm on link confirm. In HIP-initiated linking your system links each visit itself, using a link token. The token request fails if the name does not match the ABHA record exactly, middle name included, and the OTP for user-initiated linking goes by SMS, so production needs an approved SMS provider.
Wall 5: data from many systems
A real hospital runs a HIS, a lab system, radiology, pharmacy, billing and often a separate EMR. When a health information request arrives, the records for one care context may sit in four of them. Decide early which system owns each part of the record, and build the bundle from a single place that can reach them all, with a fallback when one is slow; the 202 has already gone out and the push has a time window.
HIP-initiated linking, step by step
# HIP-initiated linking, step 1: ask for a link token (it arrives on your callback)
curl -X POST https://dev.abdm.gov.in/api/hiecm/v3/token/generate-token \
-H "Authorization: Bearer $TOKEN" -H "REQUEST-ID: $(uuidgen)" -H "TIMESTAMP: $(date -u +%FT%T.000Z)" \
-H "X-CM-ID: sbx" -H "X-HIP-ID: $HIP_ID" -H "Content-Type: application/json" \
-d '{"abhaNumber": 91000000000000, "abhaAddress": "test.patient@sbx",
"name": "Test Patient Full Name", "gender": "M", "yearOfBirth": 1990}'
# Step 2: link the visit with the token in the X-LINK-TOKEN header
# POST /api/hiecm/hip/v3/link/carecontext Store the link token against the patient and reuse it for later visits instead of asking for a new one each time. Link one care context per call: in production we saw batched link calls lose records, and one call per visit also makes failures easy to retry.
Handling M2 callbacks: the flowchart that passes testing
ABDM calls these paths on your bridge URL for M2. Log every incoming path during development; a single wrong route is invisible until a test fails.
| Callback | Path on your bridge URL | Your reply |
|---|---|---|
| Link token (HIP-initiated) | /hip/v3/links/link/token | Store the token |
| Patient discovery | /user-initiated-linking/v3/patient/care-context/discover | .../on-discover |
| Link init | /user-initiated-linking/v3/link/care-context/init | .../on-init |
| Link confirm | /user-initiated-linking/v3/link/care-context/confirm | .../on-confirm |
| Consent notification | /consent/v3/request/hip/notify | /consent/v3/request/hip/on-notify |
| Health information request | /hip/health-information/request | /data-flow/v3/health-information/hip/on-request, then push and notify |
| Scan and Share | /hip/patient/share | /patient-share/v3/on-share with a token number |
| ABHA deactivation | /hip/v3/links/patient/abha/deactivate | Remove the link, keep clinical data |
# Register or update your bridge URL (sandbox gateway)
curl -X PATCH https://dev.abdm.gov.in/api/hiecm/gateway/v3/bridge/url \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-stable-domain.example/abdm"}'
# Expect 202 Accepted. Then wait: in our testing, changes took 5 to 15 minutes to apply. // Express: answer 202 first, then reply on the matching on-* API
app.post('/user-initiated-linking/v3/patient/care-context/discover', (req, res) => {
res.status(202).end(); // respond before doing any work
const requestId = req.get('REQUEST-ID') || req.get('request-id');
queue.add(async () => {
const match = await findCareContexts(req.body.patient); // your patient search
await gateway.post('/user-initiated-linking/v3/patient/care-context/on-discover', {
transactionId: req.body.transactionId,
patient: match, // referenceNumber, display, careContexts[]
response: { requestId }, // must echo the incoming request ID
});
});
}); More failure patterns and their fixes are in our bridge URL and callback debugging guide.
ABDM M2 code: an OPConsultRecord FHIR bundle
This is a minimal outpatient consultation bundle with the NRCeS profiles declared, urn:uuid: references and the Composition sections for the record type. The names and identifiers are synthetic.
{
"resourceType": "Bundle",
"id": "op-consult-example-01",
"meta": {
"versionId": "1",
"lastUpdated": "2026-04-02T10:30:00.000Z",
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle"
],
"security": [{
"system": "http://terminology.hl7.org/CodeSystem/v3-Confidentiality",
"code": "V",
"display": "very restricted"
}]
},
"identifier": {
"system": "http://hip.in",
"value": "op-consult-example-01"
},
"type": "document",
"timestamp": "2026-04-02T10:30:00.000Z",
"entry": [
{
"fullUrl": "urn:uuid:comp-001",
"resource": {
"resourceType": "Composition",
"id": "comp-001",
"meta": {
"versionId": "1",
"lastUpdated": "2026-04-02T10:30:00.000Z",
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/OPConsultRecord"
]
},
"identifier": {
"system": "https://ndhm.in/phr",
"value": "comp-001"
},
"status": "final",
"type": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "371530004",
"display": "Clinical consultation report"
}],
"text": "Clinical consultation report"
},
"subject": {
"reference": "urn:uuid:pat-001",
"display": "Rajesh Kumar"
},
"encounter": {
"reference": "urn:uuid:enc-001",
"display": "OPD Visit"
},
"date": "2026-04-02T10:30:00.000Z",
"author": [{
"reference": "urn:uuid:prac-001",
"display": "Dr. Priya Sharma"
}],
"title": "OP Consultation",
"custodian": {
"reference": "urn:uuid:org-001",
"display": "City General Hospital"
},
"section": [
{
"title": "Chief Complaints",
"code": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "422843007",
"display": "Chief complaint section"
}]
},
"entry": [{
"reference": "urn:uuid:cond-chief-001",
"display": "Persistent fever with body ache"
}]
},
{
"title": "Allergies",
"code": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "722446000",
"display": "Allergy record"
}]
},
"entry": [{
"reference": "urn:uuid:allergy-001",
"display": "Penicillin allergy"
}]
},
{
"title": "Medical History",
"code": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "371529009",
"display": "History and physical report"
}]
},
"entry": [{
"reference": "urn:uuid:cond-history-001",
"display": "Type 2 Diabetes Mellitus"
}]
},
{
"title": "Medications",
"code": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "721912009",
"display": "Medication summary document"
}]
},
"entry": [
{
"reference": "urn:uuid:medreq-001",
"display": "Azithromycin 500mg"
},
{
"reference": "urn:uuid:medreq-002",
"display": "Paracetamol 650mg"
}
]
},
{
"title": "Follow Up",
"code": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "736271009",
"display": "Follow up encounter"
}]
},
"entry": [{
"reference": "urn:uuid:appt-001",
"display": "Follow-up in 7 days"
}]
}
]
}
},
{
"fullUrl": "urn:uuid:prac-001",
"resource": {
"resourceType": "Practitioner",
"id": "prac-001",
"meta": {
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner"
]
},
"identifier": [{
"type": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MD",
"display": "Medical License number"
}]
},
"system": "https://doctor.ndhm.gov.in",
"value": "DOC-12345"
}],
"name": [{ "text": "Dr. Priya Sharma" }]
}
},
{
"fullUrl": "urn:uuid:org-001",
"resource": {
"resourceType": "Organization",
"id": "org-001",
"meta": {
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization"
]
},
"identifier": [{
"type": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "PRN",
"display": "Provider number"
}]
},
"system": "https://facility.ndhm.gov.in",
"value": "IN2710004770"
}],
"name": "City General Hospital"
}
},
{
"fullUrl": "urn:uuid:pat-001",
"resource": {
"resourceType": "Patient",
"id": "pat-001",
"meta": {
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient"
]
},
"identifier": [{
"type": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR",
"display": "Medical record number"
}]
},
"system": "https://healthid.ndhm.gov.in",
"value": "91-7345-1816-0779"
}],
"name": [{ "text": "Rajesh Kumar" }],
"gender": "male",
"birthDate": "1985-06-15"
}
},
{
"fullUrl": "urn:uuid:enc-001",
"resource": {
"resourceType": "Encounter",
"id": "enc-001",
"meta": {
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/Encounter"
]
},
"identifier": [{
"system": "https://ndhm.in",
"value": "enc-001"
}],
"status": "finished",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "AMB",
"display": "ambulatory"
},
"subject": {
"reference": "urn:uuid:pat-001",
"display": "Rajesh Kumar"
},
"period": {
"start": "2026-04-02T09:00:00.000Z",
"end": "2026-04-02T09:30:00.000Z"
}
}
},
{
"fullUrl": "urn:uuid:cond-chief-001",
"resource": {
"resourceType": "Condition",
"id": "cond-chief-001",
"meta": {
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition"
]
},
"clinicalStatus": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
"code": "active",
"display": "Active"
}]
},
"code": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "386661006",
"display": "Fever"
}],
"text": "Persistent fever with body ache for 3 days"
},
"subject": {
"reference": "urn:uuid:pat-001",
"display": "Rajesh Kumar"
}
}
},
{
"fullUrl": "urn:uuid:allergy-001",
"resource": {
"resourceType": "AllergyIntolerance",
"id": "allergy-001",
"meta": {
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/AllergyIntolerance"
]
},
"clinicalStatus": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical",
"code": "active",
"display": "Active"
}]
},
"verificationStatus": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification",
"code": "confirmed",
"display": "Confirmed"
}]
},
"code": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "91936005",
"display": "Allergy to penicillin"
}],
"text": "Penicillin allergy"
},
"patient": {
"reference": "urn:uuid:pat-001",
"display": "Rajesh Kumar"
},
"recordedDate": "2026-04-02T10:30:00.000Z",
"recorder": {
"reference": "urn:uuid:prac-001",
"display": "Dr. Priya Sharma"
}
}
},
{
"fullUrl": "urn:uuid:cond-history-001",
"resource": {
"resourceType": "Condition",
"id": "cond-history-001",
"meta": {
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition"
]
},
"clinicalStatus": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
"code": "active",
"display": "Active"
}]
},
"code": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "44054006",
"display": "Type 2 diabetes mellitus"
}],
"text": "Type 2 Diabetes Mellitus: diagnosed 2019, on Metformin 500mg BD"
},
"subject": {
"reference": "urn:uuid:pat-001",
"display": "Rajesh Kumar"
}
}
},
{
"fullUrl": "urn:uuid:medreq-001",
"resource": {
"resourceType": "MedicationRequest",
"id": "medreq-001",
"meta": {
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/MedicationRequest"
]
},
"status": "active",
"intent": "order",
"medicationCodeableConcept": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "96034006",
"display": "Azithromycin"
}],
"text": "Azithromycin 500mg"
},
"subject": {
"reference": "urn:uuid:pat-001",
"display": "Rajesh Kumar"
},
"encounter": {
"reference": "urn:uuid:enc-001",
"display": "OPD Visit"
},
"authoredOn": "2026-04-02T10:30:00.000Z",
"requester": {
"reference": "urn:uuid:prac-001",
"display": "Dr. Priya Sharma"
},
"reasonReference": [{
"reference": "urn:uuid:cond-chief-001",
"display": "Fever"
}],
"dosageInstruction": [{
"text": "One tablet once daily for 5 days",
"route": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "26643006",
"display": "Oral route"
}],
"text": "Oral"
}
}]
}
},
{
"fullUrl": "urn:uuid:medreq-002",
"resource": {
"resourceType": "MedicationRequest",
"id": "medreq-002",
"meta": {
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/MedicationRequest"
]
},
"status": "active",
"intent": "order",
"medicationCodeableConcept": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "387517004",
"display": "Paracetamol"
}],
"text": "Paracetamol 650mg"
},
"subject": {
"reference": "urn:uuid:pat-001",
"display": "Rajesh Kumar"
},
"encounter": {
"reference": "urn:uuid:enc-001",
"display": "OPD Visit"
},
"authoredOn": "2026-04-02T10:30:00.000Z",
"requester": {
"reference": "urn:uuid:prac-001",
"display": "Dr. Priya Sharma"
},
"dosageInstruction": [{
"text": "One tablet every 6 hours as needed for fever",
"route": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "26643006",
"display": "Oral route"
}],
"text": "Oral"
}
}]
}
},
{
"fullUrl": "urn:uuid:appt-001",
"resource": {
"resourceType": "Appointment",
"id": "appt-001",
"meta": {
"profile": [
"https://nrces.in/ndhm/fhir/r4/StructureDefinition/Appointment"
]
},
"status": "booked",
"serviceType": [{
"coding": [{
"system": "http://snomed.info/sct",
"code": "11429006",
"display": "Consultation"
}]
}],
"appointmentType": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/v2-0276",
"code": "FOLLOWUP",
"display": "A follow up visit"
}]
},
"reasonReference": [{
"reference": "urn:uuid:cond-chief-001",
"display": "Follow up for fever"
}],
"description": "Follow-up consultation: reassess fever, review blood work",
"start": "2026-04-09T10:00:00.000Z",
"end": "2026-04-09T10:30:00.000Z",
"participant": [
{
"actor": {
"reference": "urn:uuid:prac-001",
"display": "Dr. Priya Sharma"
},
"status": "accepted"
},
{
"actor": {
"reference": "urn:uuid:pat-001",
"display": "Rajesh Kumar"
},
"status": "accepted"
}
]
}
}
]
} ABDM Fidelius code: encrypt a record for the data push
A simplified version of the implementation in our TypeScript ABDM SDK. Step 1 generates your key pair and nonce, step 2 derives the shared secret, step 3 derives the salt and IV and encrypts, step 4 formats the data push entry.
import crypto from 'crypto';
import elliptic from 'elliptic';
import BN from 'bn.js';
// BouncyCastle Curve25519: Weierstrass form (NOT Montgomery X25519)
const bc25519 = new elliptic.curve.short({
p: '7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed',
a: '2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA984914A144',
b: '7B425ED097B425ED097B425ED097B425ED097B4260B5E9C7710C864',
n: '1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed',
g: [
'2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad245a',
'20ae19a1b8a086b4e01edd2c7748d14c923d4d7e6d7c61b229e9c5a27eced3d9',
],
h: '08',
});
const G = bc25519.g;
const N = bc25519.n;
function generateKeyMaterial() {
let privBN;
do {
privBN = new BN(crypto.randomBytes(32));
} while (privBN.isZero() || privBN.gte(N));
const pubPoint = G.mul(privBN);
const xBuf = pubPoint.getX().toArrayLike(Buffer, 'be', 32);
const yBuf = pubPoint.getY().toArrayLike(Buffer, 'be', 32);
const privBuf = privBN.toArrayLike(Buffer, 'be', 32);
// Uncompressed public key: 0x04 + x + y
const publicKey = Buffer.concat([Buffer.from([0x04]), xBuf, yBuf]);
// X.509 DER encoding: fixed ASN.1 prefix + x + y
const x509Prefix = Buffer.from(X509_PREFIX_BASE64, 'base64');
const x509PublicKey = Buffer.concat([x509Prefix, xBuf, yBuf]);
const nonce = crypto.randomBytes(32);
return {
privateKey: privBuf.toString('base64'),
publicKey: publicKey.toString('base64'),
x509PublicKey: x509PublicKey.toString('base64'),
nonce: nonce.toString('base64'),
};
} function computeSharedSecret(privateKeyB64, publicKeyB64) {
const privBN = new BN(Buffer.from(privateKeyB64, 'base64'));
const pubBuf = Buffer.from(publicKeyB64, 'base64');
// Decode public key: handle both raw (65-byte) and X.509 DER formats
let x, y;
if (pubBuf.length === 65) {
x = pubBuf.subarray(1, 33);
y = pubBuf.subarray(33, 65);
} else {
// X.509: last 64 bytes are x + y
x = pubBuf.subarray(pubBuf.length - 64, pubBuf.length - 32);
y = pubBuf.subarray(pubBuf.length - 32);
}
const pubPoint = bc25519.point(new BN(x), new BN(y));
const sharedPoint = pubPoint.mul(privBN);
return sharedPoint.getX().toArrayLike(Buffer, 'be', 32);
} function encrypt({ senderNonce, requesterNonce, senderPrivateKey,
requesterPublicKey, plaintext }) {
const sNonce = Buffer.from(senderNonce, 'base64');
const rNonce = Buffer.from(requesterNonce, 'base64');
// XOR the two 32-byte nonces
const xorNonces = Buffer.alloc(32);
for (let i = 0; i < 32; i++) {
xorNonces[i] = sNonce[i] ^ rNonce[i];
}
// First 20 bytes = HKDF salt, last 12 bytes = AES-GCM IV
const salt = xorNonces.subarray(0, 20);
const iv = xorNonces.subarray(20, 32);
// ECDH shared secret
const sharedSecret = computeSharedSecret(senderPrivateKey, requesterPublicKey);
// HKDF-SHA256: derive 32-byte AES key
const aesKey = Buffer.from(
crypto.hkdfSync('sha256', sharedSecret, salt, Buffer.alloc(0), 32)
);
// AES-256-GCM encrypt
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf-8'),
cipher.final(),
]);
const authTag = cipher.getAuthTag(); // 16 bytes
// Concatenate ciphertext + auth tag, then base64 encode
return Buffer.concat([encrypted, authTag]).toString('base64');
} function formatForDataPush(encryptedContent, hipKeyMaterial, careContextRef) {
return {
pageNumber: 0,
pageCount: 1,
transactionId: transactionId,
entries: [{
content: encryptedContent,
media: 'application/fhir+json',
checksum: crypto.createHash('md5').update(encryptedContent).digest('hex'),
careContextReference: careContextRef,
}],
keyMaterial: {
cryptoAlg: 'ECDH',
curve: 'Curve25519',
dhPublicKey: {
expiry: new Date(Date.now() + 86400000).toISOString(),
parameters: 'Curve25519/32byte random key',
keyValue: hipKeyMaterial.x509PublicKey, // X.509 DER format
},
nonce: hipKeyMaterial.nonce,
},
};
} Test your implementation against a reference before a live push: a round trip against the Java fidelius-cli or the Python pyfidelius implementation catches curve and nonce mistakes in minutes.
The ABDM M2 certification checklist
Phase 1: foundation
- M1 working: ABHA creation, verification and the ABHA card, plus your facility registered in HFR so it appears in PHR search.
- A stable public bridge URL registered, with callbacks confirmed arriving.
- Catch-all logging on every M2 callback path; 202 returned immediately on all of them.
- Request ID extracted from every incoming call and echoed in every reply.
Phase 2: linking and consent
- Discovery answered with care contexts (reference number, display, type).
- User-initiated link init and confirm working end to end with the PHR app, OTP by SMS.
- HIP-initiated linking: link token requested, stored, reused; one care context per link call.
- Consent notifications acknowledged within seconds; revoked consents deleted; expired consents handled on schedule.
Phase 3: records
- FHIR bundle builders for every record type your facility produces, validated against the NRCeS profiles.
- Fidelius encryption verified by round trip against a reference implementation.
- Data push with retry inside the time window, then TRANSFERRED with a status per care context.
Phase 4: testing and exit
- Every M2 test case run internally with the sandbox PHR app before NHA's testing.
- Functional testing with NHA, the WASA security audit and the demo; start the security audit early because remediation takes time.
The stages after testing are explained in our ABDM certification process guide.
Build M2 yourself or use an SDK?
| M2 component | Building from scratch means | A mature SDK gives you |
|---|---|---|
| FHIR bundles | Writing and validating builders for each record type and profile | Tested builders with profiles and references wired |
| Fidelius | Implementing custom-curve ECDH, key encoding and nonce handling | An implementation already verified against the reference |
| Callbacks | Routing, request ID correlation, queues and retries | Handlers for every path with correlation built in |
| Linking | Token storage, OTP flows and SMS | The flows wired to your patient model |
| Testing | Learning the test sheet case by case | Behaviour aligned with a system that already passed |
The trade-off is ownership: an SDK saves the learning curve, your team still owns the mapping from your database to FHIR. Our build vs buy vs SDK guide compares the options in depth, and the HIP reference architecture shows the full design.
What we learned taking hospital software through M2
- Follow a certified reference click by click. Where our behaviour differed from a system NHA had already certified, matching it was faster than arguing a test case.
- Callbacks are the product. Lost callbacks and tunnels that dropped requests caused more failed runs than any API detail. We probe the endpoint with a real-size request before every test session.
- One care context per link call. Batched links lost records in production.
- Plan the SMS provider before testing. Without it, user-initiated linking cannot send the OTP.
- Prove the data end to end. In a live test in September 2026 we checked every value a requester received against the source: 526 of 526 values and 30 of 30 attachments arrived intact.
Stuck on M2? Our ABDM integration engineers can review your callbacks, FHIR bundles and Fidelius code against NHA's test cases, or build M2 into your HMIS. Talk to our team.
Ready to scale?
Talk to our healthcare engineering team about building, integrating, and shipping faster.
Frequently Asked Questions
What is ABDM M2?
What does NHA test for ABDM M2 certification?
Can we do M1 and M3 without M2?
Where is the ABDM M2 documentation?
Why does Fidelius encryption fail with standard X25519 libraries?
How long does a bridge URL change take to apply?
What happens if a data push fails?
Is the WASA security audit needed for M2?


