ABDM V3 — Manual Backend Test Plan
Date: 2026-07-06 · Branch: feature/abdm_integration_port
Target: Kulcare ABDM backend (/integration/abdm/*), V3 APIs
Official reference: ABDM sandbox v3 docs — https://sandbox.abdm.gov.in/sandbox/v3/new-documentation
Milestones per official docs: M1 = ABHA creation/registration · M2 = HIP patient discovery & linking · M3 = Consent & data transfer. This plan groups tests to match how the team thinks about the flow (M1 = clinic HIP setup + ABHA registration + add patient; M2 = HIP linking; M3 = consent request + data).
Companion files:
- Architecture/where-things-live:
docs/audits/abdm-parts-to-domains-audit-2026-07-06.md - Ready-to-run requests:
docs/testing/kulcare-abdm.postman_collection.json
0. Prerequisites
0.1 Environment
- Backend running locally (
rails s, defaulthttp://localhost:3000) pointed at ABDM sandbox credentials. Rails.application.credentialspopulated for sandbox:client_id,client_secret,gateway_url,cm_id/abdm_cm_id, and the nestedabdm.*hosts (abdm_hid_host_v3,abdm_gateway_host_v3_modes,abdm_gateway_host_m3,abdm_gateway_host_hiecm,abdm_health_id_number_search,abdm_abha_address_search). (See audit §8.)rails db:migraterun so allabdm_*tables exist.- Webhook endpoints publicly reachable from ABDM sandbox (use the dev Cloudflare tunnel helper —
chore(dev): per-developer Cloudflare webhook tunnel helper). Record the tunnel URL; ABDM must be able to call/integration/abdm/webhooks/*.
0.2 Authentication (every non-webhook request needs these)
| Header / param | Value |
|---|---|
Authorization | Bearer {{token}} — a real CMS user JWT (login via the app or the auth endpoint) |
facility_id | the clinic's facility UUID (query/body param, as shown per test) |
| Tenant | resolved automatically from the authenticated user's membership |
The ABDM gateway secrets are server-side only — never put ABDM
client_id/client_secretin Postman. You are testing Kulcare's endpoints, which internally call ABDM.
0.3 Test data you'll need from the ABDM sandbox
- A sandbox Aadhaar number + its mobile (for M1 Aadhaar OTP).
- A pre-existing sandbox ABHA number/address to be linked (for M2) — e.g. one created in M1.
- The HIP service_id ABDM issued when you registered the facility in the sandbox bridge (for M1 facility registration). If unknown, the
bridge-servicescall returns the list.
0.4 Conventions in this plan
{{...}}= Postman environment variable (pre-set or auto-captured by collection test scripts).- All bodies are JSON (
Content-Type: application/json). - Pass criteria = the minimum to mark a step green; also assert the ABDM side reflects the state.
MILESTONE 1 — Clinic HIP setup + ABHA registration + add patient
M1.1 Register facility as HIP (Phase 1)
POST /integration/abdm/facility_registrations
{
"facility_registration": {
"facility_id": "{{facility_id}}",
"service_id": "SBX_001751",
"registration_type": "hip",
"active": true
}
}- Internally:
Hip::BridgeServicevalidatesservice_idis a HIP via/api/hiecm/gateway/v3/bridge-services. - Pass:
201, row created withregistration_type=hip,active=true. Captureid→{{facility_registration_id}}. - Fail modes: service_id is HIU → "registered as HIU, not HIP"; not in registry → "not found in ABDM registry".
M1.2 (Optional) Register same facility as HIU
Same endpoint, "registration_type": "hiu". Required before M3 (consent requests need an HIU registration).
M1.3 List / fetch facility registrations
- GET
/integration/abdm/facility_registrations?facility_id={{facility_id}}→ 200, array incl. your registrations. - GET
/integration/abdm/facility_registrations/{{facility_registration_id}}→ 200, detailed view.
M1.4 ABHA registration — Step 1: send Aadhaar OTP
POST /integration/abdm/abha/registrations/send_otp
{ "aadhaar": "999988887777" }- Internally: creates
AbdmAuthSession(registration_aadhaar), RSA-encrypts Aadhaar, calls/v3/enrollment/request/otp. - Pass:
200/201, returnstxn_id. Capture →{{session_id}}(the auth session id) and the ABDMtxn_idis stored server-side. - Note: Aadhaar OTP goes to the mobile linked to that Aadhaar in the sandbox.
M1.5 ABHA registration — Step 2: verify OTP & enroll
POST /integration/abdm/abha/registrations/verify_otp
{ "session_id": "{{session_id}}", "otp": "123456", "mobile": "9999900000" }- Internally: calls
/v3/enrollment/enrol/byAadhaar; session →otp_verified; storesABHANumber. - Pass: returns
ABHANumber+ newtxn_id.
M1.6 ABHA registration — Step 3: suggest address
POST /integration/abdm/abha/registrations/suggest_address
{ "session_id": "{{session_id}}" }- Pass: returns
abhaAddressList(suggested@abdmaddresses).
M1.7 ABHA registration — Step 4: create ABHA address
POST /integration/abdm/abha/registrations/create_address
{ "session_id": "{{session_id}}", "abha_address": "mytestclinic@abdm" }- Internally:
/v3/enrollment/enrol/abha-address; session →completed. - Pass: returns final
abhaAddress+ABHANumber. Capture{{abha_address}},{{abha_number}}.
M1.8 Link ABHA profile to a clinical patient
POST /integration/abdm/abha/profiles
{
"patient_profile": {
"patient_id": "{{patient_id}}",
"abha_address": "{{abha_address}}",
"abha_number": "{{abha_number}}",
"registration_status": "verified"
}
}- Internally: creates
AbdmPatientProfile+ denormalisesabha_idonto the clinical patient. Prerequisite for M2/M3. - Pass:
201, profileregistration_status=verified.
M1.9 PHR login flow (alternative auth → get x_token)
- POST
/integration/abdm/phr/sessions/send_otp→{ "method": "abha", "login_id": "{{abha_address}}", "patient_id": "{{patient_id}}" } - POST
/integration/abdm/phr/sessions/verify_otp→{ "session_id": "{{session_id}}", "otp": "123456" } - POST
/integration/abdm/phr/sessions/verify_user→{ "session_id": "{{session_id}}", "abha_address": "{{abha_address}}" } - POST
/integration/abdm/phr/sessions/refresh_token→{ "r_token": "{{refresh_token}}" } - Pass: verify steps return tokens;
x_tokencaptured for profile reads.
M1.10 Read ABHA profile / QR / card
- POST
/integration/abdm/abha/profiles/{{profile_id}}/fetch_profile→{ "x_token": "{{x_token}}" } - GET
/integration/abdm/abha/profiles/{{profile_id}}/qr_code→ image/data. - GET
/integration/abdm/abha/profiles/{{profile_id}}/card→ ABHA card. - GET
/integration/abdm/abha/profiles/{{profile_id}}→ stored profile.
MILESTONE 2 — HIP linking (link a patient's ABHA to this clinic's care contexts)
Official V3 note: discovery is synchronous in V3. HIP-initiated linking here uses the auth-mode → OTP → link-token → add-contexts flow.
M2.1 Fetch auth modes for a patient by ABHA
POST /integration/abdm/link_requests/fetch_modes
{
"patient_id": "{{patient_id}}",
"health_id": "{{abha_number_or_address}}",
"abha_number": "{{abha_number}}",
"abha_address": "{{abha_address}}"
}- Internally:
Hip::LinkService#fetch_auth_modessearches by number (/health_id_number_search) or address (/abha_address_search); createsAbdmLinkRequest→auth_pending. - Pass: returns available auth modes; capture
{{link_request_id}}.
M2.2 Auth init — request OTP
POST /integration/abdm/link_requests/auth_init
{ "link_request_id": "{{link_request_id}}", "auth_mode": "mobile" }(auth_mode ∈ mobile | aadhaar.)
- Internally:
/v3/profile/login/request/otp; stores ABDMtxn_idin metadata. - Pass: returns
txn_id; OTP sent to patient.
M2.3 Auth confirm — verify OTP
POST /integration/abdm/link_requests/auth_confirm
{ "link_request_id": "{{link_request_id}}", "otp": "123456" }- Internally:
/v3/profile/login/verify; link_request →auth_confirmed. - Pass: returns auth response.
M2.4 Generate link token
In the current
Hip::LinkService, token generation is a distinct step (generate_token,/v3/token/generate-token) before adding contexts. If the controller auto-runs it after confirm, this step is implicit — verify via the link_requeststatus(token_generated) andlink_tokenpresence.
- Pass: link_request has
link_token+link_token_expires_at(30 min).
M2.5 Add care contexts
POST /integration/abdm/link_requests/add_contexts
{
"link_request_id": "{{link_request_id}}",
"care_contexts": [
{ "reference_number": "enc-123", "display": "OPD Visit 2026-07-06", "hi_type": "OPConsultation", "count": 1 }
]
}- Internally:
/api/hiecm/hip/v3/link/carecontextwithX-LINK-TOKEN; link_request →contexts_adding; patient gets SMS (stubbed for now). - Pass:
200;AbdmCareContextrows created; ABDM sandbox shows the care context linked to the patient's ABHA.
M2.6 Inspect link requests & care contexts
- GET
/integration/abdm/link_requests?patient_id={{patient_id}}→ list, statuslinked. - GET
/integration/abdm/link_requests/{{link_request_id}}→ detailed (token, contexts). - GET
/integration/abdm/care_contexts?patient_id={{patient_id}}and/:id→ created contexts.
M2.5b SMS Notification to Patient
POST /integration/abdm/link_requests/sms_notify
{
"link_request_id": "{{link_request_id}}"
}- Internally: calls ABDM
POST /api/hiecm/hip/v3/link/patient/links/sms/notify2; ABDM sends SMS to patient's registered mobile. - Pass:
202; patient receives SMS with deep link to view records on PHR app. - Reference: ABDM M2 test case
HIP_INIT_NOTIFY_HIECM(row 55 of M2 XLSX).
M2.5c Notify Care Context Update
POST /integration/abdm/link_requests/notify_context_update
{
"link_request_id": "{{link_request_id}}",
"care_context_id": "{{care_context_id}}"
}- Internally: calls ABDM
POST /api/hiecm/hip/v3/link/context/notify; ABDM notifies all subscribed HIUs. - Pass:
202; ABDM callback atPOST /api/v3/links/context/on-notifyreturnsacknowledgement.status = SUCCESS. - Reference: ABDM M2 test case
HIP_INTI_LINK_501(row 40 of M2 XLSX).
M2.5d Get Patient Links
GET /integration/abdm/link_requests/patient_links?abha_address={{abha_address}}
- Internally: calls ABDM
GET /api/hiecm/hip/v3/link/patient/links. - Pass:
200; returnspatient.links[]with all HIPs and their care contexts for this ABHA address.
M2.7 (Webhook side) HIP link callbacks from ABDM
ABDM sandbox will call back (via your tunnel):
POST /integration/abdm/webhooks/hip/on_token_generatePOST /integration/abdm/webhooks/hip/on_add_contextsPOST /integration/abdm/webhooks/hip/auth_notifyPOST /integration/abdm/webhooks/hip/profile_sharePOST /integration/abdm/webhooks/discovery/discoverPOST /integration/abdm/webhooks/discovery/link_initPOST /integration/abdm/webhooks/discovery/link_confirm- Pass:
200ack;AbdmCallbackrows queued +ProcessCallbackJobupdates state.
M2.8 — NHA Official V3 API Test Cases
From M2_BUILDING_HIP_WITH_APIS_UPDATED_22_Aug_*.xlsx — column V3 APIs (row 40):
V3 APIs for Demographic Auth (HIP Initiated Linking)
ABDM test case: HIP_INTI_LINK_501
| # | ABDM Endpoint | Kulcare endpoint | Status |
|---|---|---|---|
| 1 | POST /api/hiecm/v3/token/generate-token | POST /integration/abdm/link_requests/generate_token | ✅ Implemented |
| 2 | POST /api/hiecm/hip/v3/link/carecontext | POST /integration/abdm/link_requests/add_contexts | ✅ Implemented |
| 3 | POST /api/hiecm/hip/v3/link/context/notify | (planned — call after care context update) | ⬜ TODO |
| 4 | POST /api/hiecm/hip/v3/link/patient/links/sms/notify2 | (planned — SMS deep link to patient) | ⬜ TODO |
V3 APIs for Patient-Initiated Discovery
ABDM test case: USER_INIT_LINK_601 (row 47)
| # | ABDM Endpoint | Kulcare endpoint | Status |
|---|---|---|---|
| 1 | GET /api/hiecm/user-initiated-linking/v3/patient/care-context/discover | ABDM → POST /webhooks/discovery/discover | ✅ Implemented |
| 2 | POST /api/hiecm/user-initiated-linking/v3/link/care-context/init | ABDM → POST /webhooks/discovery/link_init | ✅ Implemented |
| 3 | POST /api/hiecm/user-initiated-linking/v3/link/care-context/confirm | ABDM → POST /webhooks/discovery/link_confirm | ✅ Implemented |
V3 APIs for Consent & Data Transfer (HIP Side)
ABDM test case: HIP_INIT_SHARE_CARECONTEXT (row 57)
| # | ABDM Endpoint | Kulcare endpoint | Status |
|---|---|---|---|
| 1 | POST /api/hiecm/consent/v3/request/hip/on-notify | ABDM → POST /webhooks/hip/consent_notify | ✅ Implemented |
| 2 | POST /api/hiecm/data-flow/v3/health-information/hip/on-request | ABDM → POST /webhooks/hip/on_request | ✅ Implemented |
| 3 | POST /data/transfer/v0.5 (HIP pushes encrypted bundle) | (planned — Fidelius encryption + POST) | ⬜ TODO |
| 4 | POST /api/hiecm/data-flow/v3/health-information/notify | (planned — notify transfer status) | ⬜ TODO |
M3.8 (NHA Official V3 API Test Cases)
From M3_BUILDING_HIU_WITH_APIS_UPDATED_22_August_*.xlsx — column V3 APIs (row 7):
ABDM test case: HIU_FLOW_101 (full consent + data flow)
| # | ABDM Endpoint | Kulcare endpoint | Status |
|---|---|---|---|
| 1 | POST /api/hiecm/consent/v3/request/init | POST /integration/abdm/consents | ✅ Implemented |
| 2 | GET /api/hiecm/consent/v3/request/status | (planned — check consent status) | ⬜ TODO |
| 3 | POST /api/hiecm/consent/v3/request/hiu/on-notify | ABDM → POST /webhooks/hiu/consent_notify | ✅ Implemented |
| 4 | GET /api/hiecm/consent/v3/fetch | (planned — fetch consent artefact) | ⬜ TODO |
| 5 | POST /api/hiecm/data-flow/v3/health-information/request | ABDM → POST /webhooks/hiu/on_fetch | ✅ Implemented (on_fetch) |
| 6 | POST /api/hiecm/data-flow/v3/health-information/notify | ABDM → POST /webhooks/hiu/data_notification | ✅ Implemented |
⬜ TODO items = not yet implemented in
domains/integration.
MILESTONE 3 — Consent request + data exchange
Direction note: the consent request is HIU-initiated (
Hiu::ConsentRequestService→/consent/v3/request/init). HIP is the data provider after the patient grants. OurAbdmConsent.directionsupports both.
M3.1 Create HIU consent request
POST /integration/abdm/consents
{
"patient_id": "{{patient_id}}",
"purpose": "care_management",
"hi_types": ["OPConsultation", "Prescription"],
"date_range_from": "2025-01-01T00:00:00Z",
"date_range_to": "2026-07-06T00:00:00Z",
"data_erase_at": "2027-07-06T00:00:00Z"
}- Internally: validates patient has verified ABHA + facility is HIU-registered; calls
/consent/v3/request/init; createsAbdmConsent(direction=hiu) with ABDMrequestId. - Valid
purpose:care_management|break_the_glass|public_health|healthcare_payment|disease_research|patient_requested. - Pass:
201, returnsconsent_id. Capture{{consent_id}}. Patient sees the request in the PHR/sandbox. - Fail modes: patient without verified ABHA; facility not HIU.
M3.2 Patient acts on consent (CM side)
- POST
/integration/abdm/cm/consents/{{consent_id}}/approve→ patient grants. - POST
/integration/abdm/cm/consents/{{consent_id}}/deny→{ "reason": "string" }. - POST
/integration/abdm/cm/consents/{{consent_id}}/revoke→{ "reason": "string" }. - Pass: state transitions;
AbdmConsentAuditLogrows written.
M3.3 Consent granted → artefact
After patient grants in the sandbox, ABDM calls back:
POST /integration/abdm/webhooks/hip/consent_notifyPOST /integration/abdm/webhooks/hiu/consent_notify- Pass:
AbdmConsentArtefactcreated (signed artefact + Fidelius keypair for HIP).
M3.4 List/fetch consent artefacts
- GET
/integration/abdm/cm/artefactsand/integration/abdm/cm/artefacts/{{artefact_id}}. - Pass: granted artefact present.
M3.5 Data request & transfer (HIU pulls, HIP pushes)
- HIU fetch: ABDM →
POST /integration/abdm/webhooks/hiu/on_fetch; ourHiuDataFetchService+HiuDataFetchJob. - HIP push: ABDM →
POST /integration/abdm/webhooks/hip/on_request;Hip::DataTransferService+HipDataTransferJobencrypt (Fidelius) and deliver FHIR bundle. - Pass:
AbdmDataTransferrows reachdelivered; HIU receives encrypted bundle.
M3.6 Read health records
- GET
/integration/abdm/health_records?fetched_only=true&consent_id={{consent_id}}→ list. - GET
/integration/abdm/health_records/{{record_id}}→ detail. - Pass: the fetched record + reference to its artefact.
M3.7 Data erasure (scheduled)
- After
data_erase_atelapses,DataErasureJobpurges the fetched data. - Pass (manual): advance clock / run job → record marked erased; data no longer retrievable.
M3.8a Get Consent Status
GET /integration/abdm/consents/:consent_id
- Internally: reads
AbdmConsentrecord. - Pass: returns current status (
requested→granted→active).
M3.8b Consent Revocation (patient-initiated)
- Patient revokes consent in PHR app.
- ABDM calls:
POST /webhooks/hiu/consent_notifywithconsent.status = REVOKED. - Pass:
AbdmConsent.status→revoked;AbdmConsentAuditLogrow written. - Reference: ABDM M3 test case
HIU_FLOW_202.
M3.8c Consent Expiry
data_erase_atelapses → ABDM callsconsent_notifywithstatus = EXPIRED.- Pass:
AbdmConsent.status→expired; data erasure job purges records. - Reference: ABDM M3 test case
HIU_FLOW_301.
M3.8 Inspect consents
- GET
/integration/abdm/consents?patient_id={{patient_id}}&direction=hiu→ list. - GET
/integration/abdm/consents/{{consent_id}}→ detailed (purpose, hiTypes, ranges, status).
Webhook / infra sanity
W.1 Heartbeat
- GET
/integration/abdm/webhooks/heartbeat→200(ABDM reachability). Run this first to confirm the tunnel + routing work.
W.2 Webhook auth
- All
/webhooks/*POST endpoints must reject unsigned/tampered payloads (verified byWebhookAuthenticatoragainst ABDM certs athttps://dev.abdm.gov.in/gateway/v0.5/certs). Send a bogus JWT → expect401.
Test execution summary matrix
| ID | Milestone | Endpoint | Method | Captures |
|---|---|---|---|---|
| M1.1 | M1 | /integration/abdm/facility_registrations | POST | facility_registration_id |
| M1.4–M1.7 | M1 | /integration/abdm/abha/registrations/* | POST | session_id, abha_address, abha_number |
| M1.8 | M1 | /integration/abdm/abha/profiles | POST | profile_id |
| M1.9 | M1 | /integration/abdm/phr/sessions/* | POST | x_token |
| M2.1–M2.5 | M2 | /integration/abdm/link_requests/* | POST | link_request_id |
| M3.1 | M3 | /integration/abdm/consents | POST | consent_id |
| M3.2 | M3 | /integration/abdm/cm/consents/:id/{approve,deny,revoke} | POST | — |
| M3.4 | M3 | /integration/abdm/cm/artefacts | GET | artefact_id |
| M3.6 | M3 | /integration/abdm/health_records | GET | record_id |
| W.1 | infra | /integration/abdm/webhooks/heartbeat | GET | — |
Known limitations / things to watch
- Dual API surface: the legacy
/abdm/*(parts) is still live. Make sure you test the new/integration/abdm/*paths (the Postman collection only contains these). - SMS notify in
Hip::LinkService#notify_patientis currently a stub ("Deferred until shared SMS service is available") — linking still succeeds; just no real SMS. - Sandbox OTP values and Aadhaar numbers come from the ABDM sandbox, not real data.
- After the recent base merge, run
rails db:migratesoabdm_*tables exist before testing. abhaApi(frontend) kept its/integration/abdm/facility_registrationsendpoint; verify backend route +Hip::BridgeServiceline up (audit §10).