ABDMABDM M2FHIR

ABDM M2 Certification in 2026: HIP Test Cases, Callbacks, FHIR and Fidelius Fixes That Unblock Dev Teams

April 2, 202622 min readUpdated Sep 29, 2026
Written by
Gulshan Prajapati
Gulshan Prajapati

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 in 2026: HIP Test Cases, Callbacks, FHIR and Fidelius Fixes That Unblock Dev Teams

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.

AreaWhat NHA checksWhere teams fail
Health record creationValid FHIR R4 bundles for each health information type, NRCeS profiles, ABHA in the patient identifierMissing fields that show as N/A in the PHR app
HIP-initiated linkingLink token by mobile or Aadhaar OTP, token received on your webhook, records visible in the PHR app, SMS deep linkName not matching the ABHA record exactly
User-initiated linkingFacility found in PHR search, discovery returns care contexts, OTP on link init, confirmation on link confirmDiscovery answered late or without the request ID
ConsentGranted consent stored and acknowledged fast, revoked consent deleted, expired consent markedNo acknowledgement, or data kept after revocation
Sharing recordsRequest acknowledged, bundles built and encrypted, pushed, TRANSFERRED notified, within the time windowFidelius errors, tunnel down at push time
Scan and Share, opt-outProfile shared by QR with a token issued; ABHA deactivation removes the link and keeps clinical dataDeleting 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, and X-HIP-ID for M2 (M3 uses X-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.

CallbackPath on your bridge URLYour reply
Link token (HIP-initiated)/hip/v3/links/link/tokenStore 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/deactivateRemove 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

  1. M1 working: ABHA creation, verification and the ABHA card, plus your facility registered in HFR so it appears in PHR search.
  2. A stable public bridge URL registered, with callbacks confirmed arriving.
  3. Catch-all logging on every M2 callback path; 202 returned immediately on all of them.
  4. Request ID extracted from every incoming call and echoed in every reply.

Phase 2: linking and consent

  1. Discovery answered with care contexts (reference number, display, type).
  2. User-initiated link init and confirm working end to end with the PHR app, OTP by SMS.
  3. HIP-initiated linking: link token requested, stored, reused; one care context per link call.
  4. Consent notifications acknowledged within seconds; revoked consents deleted; expired consents handled on schedule.

Phase 3: records

  1. FHIR bundle builders for every record type your facility produces, validated against the NRCeS profiles.
  2. Fidelius encryption verified by round trip against a reference implementation.
  3. Data push with retry inside the time window, then TRANSFERRED with a status per care context.

Phase 4: testing and exit

  1. Every M2 test case run internally with the sandbox PHR app before NHA's testing.
  2. 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 componentBuilding from scratch meansA mature SDK gives you
FHIR bundlesWriting and validating builders for each record type and profileTested builders with profiles and references wired
FideliusImplementing custom-curve ECDH, key encoding and nonce handlingAn implementation already verified against the reference
CallbacksRouting, request ID correlation, queues and retriesHandlers for every path with correlation built in
LinkingToken storage, OTP flows and SMSThe flows wired to your patient model
TestingLearning the test sheet case by caseBehaviour 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?

ABDM M2 is the milestone that makes your software a Health Information Provider (HIP). It links each patient visit to the patient's ABHA as a care context, answers discovery, linking and consent callbacks, and pushes the patient's records as Fidelius-encrypted FHIR bundles when a consent is granted.

What does NHA test for ABDM M2 certification?

NHA's M2 test cases cover FHIR record creation, HIP-initiated linking with mobile or Aadhaar OTP, user-initiated linking from a PHR app, consent grant, revoke and expiry, sharing records with encryption and a TRANSFERRED notification, Scan and Share, and ABHA opt-out.

Can we do M1 and M3 without M2?

Yes, if your product does not hold patient records. Sandbox exit needs any two milestones, so an app that only reads records can pair M1 with M3. Hospitals and clinics that create records normally need M2.

Where is the ABDM M2 documentation?

The official M2 documentation is on the ABDM sandbox developer portal, and the FHIR profiles are in the NRCeS ABDM implementation guide. Nirmitee's public ABDM V3 Postman collection and FHIR bundle examples on GitHub give ready-to-run calls and sample bundles.

Why does Fidelius encryption fail with standard X25519 libraries?

ABDM's Fidelius uses Curve25519 in the short Weierstrass form implemented by BouncyCastle, not the Montgomery form used by X25519 in Web Crypto or Node's crypto module. The shared secret differs, so the receiver cannot decrypt. Use a library that accepts custom curve parameters and test a round trip against a reference implementation.

How long does a bridge URL change take to apply?

The PATCH returns 202 immediately, but in our sandbox testing callbacks moved to the new URL only after 5 to 15 minutes. There is no status API, so confirm by triggering a callback.

What happens if a data push fails?

If the push to the requester's dataPushUrl fails, retry with backoff inside the allowed time window and report a status per care context in the TRANSFERRED notification. Log every attempt; the request ID is what sandbox support asks for.

Is the WASA security audit needed for M2?

The WASA security audit is part of the sandbox exit before production credentials are issued, not of building M2 in the sandbox. Start it early, because fixing audit findings takes time.
Share