Kulcare Docs

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, default http://localhost:3000) pointed at ABDM sandbox credentials.
  • Rails.application.credentials populated for sandbox: client_id, client_secret, gateway_url, cm_id/abdm_cm_id, and the nested abdm.* 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:migrate run so all abdm_* 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 / paramValue
AuthorizationBearer {{token}} — a real CMS user JWT (login via the app or the auth endpoint)
facility_idthe clinic's facility UUID (query/body param, as shown per test)
Tenantresolved automatically from the authenticated user's membership

The ABDM gateway secrets are server-side only — never put ABDM client_id/client_secret in 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-services call 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::BridgeService validates service_id is a HIP via /api/hiecm/gateway/v3/bridge-services.
  • Pass: 201, row created with registration_type=hip, active=true. Capture id → {{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, returns txn_id. Capture → {{session_id}} (the auth session id) and the ABDM txn_id is 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; stores ABHANumber.
  • Pass: returns ABHANumber + new txn_id.

M1.6 ABHA registration — Step 3: suggest address

POST /integration/abdm/abha/registrations/suggest_address

{ "session_id": "{{session_id}}" }
  • Pass: returns abhaAddressList (suggested @abdm addresses).

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}}.

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 + denormalises abha_id onto the clinical patient. Prerequisite for M2/M3.
  • Pass: 201, profile registration_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_token captured 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_modes searches by number (/health_id_number_search) or address (/abha_address_search); creates AbdmLinkRequest → 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 ABDM txn_id in 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.

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_request status (token_generated) and link_token presence.

  • 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/carecontext with X-LINK-TOKEN; link_request → contexts_adding; patient gets SMS (stubbed for now).
  • Pass: 200; AbdmCareContext rows created; ABDM sandbox shows the care context linked to the patient's ABHA.
  • GET /integration/abdm/link_requests?patient_id={{patient_id}} → list, status linked.
  • 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 at POST /api/v3/links/context/on-notify returns acknowledgement.status = SUCCESS.
  • Reference: ABDM M2 test case HIP_INTI_LINK_501 (row 40 of M2 XLSX).

GET /integration/abdm/link_requests/patient_links?abha_address={{abha_address}}

  • Internally: calls ABDM GET /api/hiecm/hip/v3/link/patient/links.
  • Pass: 200; returns patient.links[] with all HIPs and their care contexts for this ABHA address.

ABDM sandbox will call back (via your tunnel):

  • POST /integration/abdm/webhooks/hip/on_token_generate
  • POST /integration/abdm/webhooks/hip/on_add_contexts
  • POST /integration/abdm/webhooks/hip/auth_notify
  • POST /integration/abdm/webhooks/hip/profile_share
  • POST /integration/abdm/webhooks/discovery/discover
  • POST /integration/abdm/webhooks/discovery/link_init
  • POST /integration/abdm/webhooks/discovery/link_confirm
  • Pass: 200 ack; AbdmCallback rows queued + ProcessCallbackJob updates 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 EndpointKulcare endpointStatus
1POST /api/hiecm/v3/token/generate-tokenPOST /integration/abdm/link_requests/generate_token✅ Implemented
2POST /api/hiecm/hip/v3/link/carecontextPOST /integration/abdm/link_requests/add_contexts✅ Implemented
3POST /api/hiecm/hip/v3/link/context/notify(planned — call after care context update)⬜ TODO
4POST /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 EndpointKulcare endpointStatus
1GET /api/hiecm/user-initiated-linking/v3/patient/care-context/discoverABDM → POST /webhooks/discovery/discover✅ Implemented
2POST /api/hiecm/user-initiated-linking/v3/link/care-context/initABDM → POST /webhooks/discovery/link_init✅ Implemented
3POST /api/hiecm/user-initiated-linking/v3/link/care-context/confirmABDM → POST /webhooks/discovery/link_confirm✅ Implemented

ABDM test case: HIP_INIT_SHARE_CARECONTEXT (row 57)

#ABDM EndpointKulcare endpointStatus
1POST /api/hiecm/consent/v3/request/hip/on-notifyABDM → POST /webhooks/hip/consent_notify✅ Implemented
2POST /api/hiecm/data-flow/v3/health-information/hip/on-requestABDM → POST /webhooks/hip/on_request✅ Implemented
3POST /data/transfer/v0.5 (HIP pushes encrypted bundle)(planned — Fidelius encryption + POST)⬜ TODO
4POST /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 EndpointKulcare endpointStatus
1POST /api/hiecm/consent/v3/request/initPOST /integration/abdm/consents✅ Implemented
2GET /api/hiecm/consent/v3/request/status(planned — check consent status)⬜ TODO
3POST /api/hiecm/consent/v3/request/hiu/on-notifyABDM → POST /webhooks/hiu/consent_notify✅ Implemented
4GET /api/hiecm/consent/v3/fetch(planned — fetch consent artefact)⬜ TODO
5POST /api/hiecm/data-flow/v3/health-information/requestABDM → POST /webhooks/hiu/on_fetch✅ Implemented (on_fetch)
6POST /api/hiecm/data-flow/v3/health-information/notifyABDM → 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. Our AbdmConsent.direction supports both.

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; creates AbdmConsent (direction=hiu) with ABDM requestId.
  • Valid purpose: care_management|break_the_glass|public_health|healthcare_payment|disease_research|patient_requested.
  • Pass: 201, returns consent_id. Capture {{consent_id}}. Patient sees the request in the PHR/sandbox.
  • Fail modes: patient without verified ABHA; facility not HIU.
  • 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; AbdmConsentAuditLog rows written.

After patient grants in the sandbox, ABDM calls back:

  • POST /integration/abdm/webhooks/hip/consent_notify
  • POST /integration/abdm/webhooks/hiu/consent_notify
  • Pass: AbdmConsentArtefact created (signed artefact + Fidelius keypair for HIP).
  • GET /integration/abdm/cm/artefacts and /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; our HiuDataFetchService + HiuDataFetchJob.
  • HIP push: ABDM → POST /integration/abdm/webhooks/hip/on_request; Hip::DataTransferService + HipDataTransferJob encrypt (Fidelius) and deliver FHIR bundle.
  • Pass: AbdmDataTransfer rows reach delivered; 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_at elapses, DataErasureJob purges the fetched data.
  • Pass (manual): advance clock / run job → record marked erased; data no longer retrievable.

GET /integration/abdm/consents/:consent_id

  • Internally: reads AbdmConsent record.
  • Pass: returns current status (requested → granted → active).
  • Patient revokes consent in PHR app.
  • ABDM calls: POST /webhooks/hiu/consent_notify with consent.status = REVOKED.
  • Pass: AbdmConsent.status → revoked; AbdmConsentAuditLog row written.
  • Reference: ABDM M3 test case HIU_FLOW_202.
  • data_erase_at elapses → ABDM calls consent_notify with status = 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 by WebhookAuthenticator against ABDM certs at https://dev.abdm.gov.in/gateway/v0.5/certs). Send a bogus JWT → expect 401.

Test execution summary matrix

IDMilestoneEndpointMethodCaptures
M1.1M1/integration/abdm/facility_registrationsPOSTfacility_registration_id
M1.4–M1.7M1/integration/abdm/abha/registrations/*POSTsession_id, abha_address, abha_number
M1.8M1/integration/abdm/abha/profilesPOSTprofile_id
M1.9M1/integration/abdm/phr/sessions/*POSTx_token
M2.1–M2.5M2/integration/abdm/link_requests/*POSTlink_request_id
M3.1M3/integration/abdm/consentsPOSTconsent_id
M3.2M3/integration/abdm/cm/consents/:id/{approve,deny,revoke}POST—
M3.4M3/integration/abdm/cm/artefactsGETartefact_id
M3.6M3/integration/abdm/health_recordsGETrecord_id
W.1infra/integration/abdm/webhooks/heartbeatGET—

Known limitations / things to watch

  1. 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).
  2. SMS notify in Hip::LinkService#notify_patient is currently a stub ("Deferred until shared SMS service is available") — linking still succeeds; just no real SMS.
  3. Sandbox OTP values and Aadhaar numbers come from the ABDM sandbox, not real data.
  4. After the recent base merge, run rails db:migrate so abdm_* tables exist before testing.
  5. abhaApi (frontend) kept its /integration/abdm/facility_registrations endpoint; verify backend route + Hip::BridgeService line up (audit §10).

On this page

0. Prerequisites0.1 Environment0.2 Authentication (every non-webhook request needs these)0.3 Test data you'll need from the ABDM sandbox0.4 Conventions in this planMILESTONE 1 — Clinic HIP setup + ABHA registration + add patientM1.1 Register facility as HIP (Phase 1)M1.2 (Optional) Register same facility as HIUM1.3 List / fetch facility registrationsM1.4 ABHA registration — Step 1: send Aadhaar OTPM1.5 ABHA registration — Step 2: verify OTP & enrollM1.6 ABHA registration — Step 3: suggest addressM1.7 ABHA registration — Step 4: create ABHA addressM1.8 Link ABHA profile to a clinical patientM1.9 PHR login flow (alternative auth → get x_token)M1.10 Read ABHA profile / QR / cardMILESTONE 2 — HIP linking (link a patient's ABHA to this clinic's care contexts)M2.1 Fetch auth modes for a patient by ABHAM2.2 Auth init — request OTPM2.3 Auth confirm — verify OTPM2.4 Generate link tokenM2.5 Add care contextsM2.6 Inspect link requests & care contextsM2.5b SMS Notification to PatientM2.5c Notify Care Context UpdateM2.5d Get Patient LinksM2.7 (Webhook side) HIP link callbacks from ABDMM2.8 — NHA Official V3 API Test CasesV3 APIs for Demographic Auth (HIP Initiated Linking)V3 APIs for Patient-Initiated DiscoveryV3 APIs for Consent & Data Transfer (HIP Side)M3.8 (NHA Official V3 API Test Cases)MILESTONE 3 — Consent request + data exchangeM3.1 Create HIU consent requestM3.2 Patient acts on consent (CM side)M3.3 Consent granted → artefactM3.4 List/fetch consent artefactsM3.5 Data request & transfer (HIU pulls, HIP pushes)M3.6 Read health recordsM3.7 Data erasure (scheduled)M3.8a Get Consent StatusM3.8b Consent Revocation (patient-initiated)M3.8c Consent ExpiryM3.8 Inspect consentsWebhook / infra sanityW.1 HeartbeatW.2 Webhook authTest execution summary matrixKnown limitations / things to watch