Kulcare Docs

ABDM M2 + M3 End-to-End Testing Guide

Date: 2026-07-28 Environment: ABDM Sandbox (sbx) Patient: 019f82e1-3d73-7a4a-88c8-da423b257e53 (Simmi Badhan) Facility: 01900002-0000-7000-8000-000000000002 (HIP+HIU: IN1810000370) ngrok: balanced-loudly-midge.ngrok-free.app


Prerequisites

# Backend running
cd backend && rails server

# ngrok running (for ABDM callbacks)
ngrok http 3000

# Patient has an ABHA in the ABDM sandbox (e.g. simmi@sbx)
# You need the EXACT name, gender, year_of_birth that ABDM has on file
#   — if they don't match, link token generation fails with
#   "information you provided does not match"

M2: HIP-Initiated Linking (Token-based v3)

Flow Overview

Step 1: ABHA Verification (M1)
  CMS → POST /integration/abdm/abha/verifications/verify_otp
  ↓ (auto-chained server-side)
  Backend → POST /api/hiecm/v3/token/generate-token
  ↓
  ABDM → POST /integration/abdm/api/v3/hip/token/on-generate-token (webhook)
  ↓ stores linkToken on AbdmLinkRequest

Step 2: Care Context Linking
  Backend → POST /api/hiecm/hip/v3/link/carecontext (uses linkToken)
  ↓
  ABDM → POST /integration/abdm/api/v3/link/on_carecontext (webhook)
  ↓ patient sees care contexts in ABHA app

Via CMS UI:

  1. Go to Patient Detail → ABDM tab → Profile subtab
  2. Click "Setup ABHA" → "Register new ABHA" (or search existing)
  3. Enter ABHA address (e.g. simmi@sbx), verify via OTP
  4. Backend auto-generates link token — check response for link_token_status

Verify it worked:

cd backend && bundle exec rails runner "
ActiveRecord::Base.connection.execute('SET search_path TO public, integration')
lr = Integration::AbdmLinkRequest.where(abha_address: 'simmi@sbx').order(created_at: :desc).first
puts \"status=\#{lr.status} token_valid=\#{lr.token_valid?} token=\#{lr.link_token&.first(40)}...\"
puts \"expires=\#{lr.link_token_expires_at} error=\#{lr.error_message.inspect}\"
"

Expected: status=token_generated, token_valid=true

If failed with "information does not match":

  • The name/gender/year_of_birth sent to ABDM doesn't match their records
  • Fix: update the patient's demographic data, or use an ABHA where you know the exact details

Care contexts are linked automatically when encounters/prescriptions are created. To test manually:

cd backend && bundle exec rails runner "
ActiveRecord::Base.connection.execute('SET search_path TO public, integration, clinical')
ActiveRecord::Base.connection.execute(\"SELECT set_config('app.current_tenant_id', '01900000-0000-7000-8000-000000000001', true)\")

patient = Clinical::Patient.find('019f82e1-3d73-7a4a-88c8-da423b257e53')
facility = Identity::Facility.find('01900002-0000-7000-8000-000000000002')

# Find an encounter to link
encounter = Clinical::Encounter.where(patient_id: patient.id).first
puts \"Encounter: \#{encounter&.id}\"

if encounter
  result = Integration::Abdm::Hip::CareContextLinkingService.call(
    patient: patient, facility: facility, encounter: encounter
  )
  puts \"success=\#{result.success?}\"
  puts \"data=\#{result.data.inspect}\"
  puts \"errors=\#{result.errors.inspect}\" unless result.success?
end
"

Expected: API returns 202, then ABDM calls back on_carecontext with "Successfully Linked care context"

Verify in ABHA app: Patient should see the care context listed.


Flow Overview

Step 1: Create Consent Request (HIU → ABDM)
  CMS → POST /integration/abdm/consents { patient_id, abha_address, purpose, hi_types, ... }
  ↓
  Backend → POST /api/hiecm/consent/v3/request/init
  ↓
  ABDM → POST /integration/abdm/api/v3/hiu/consent/request/on-init (webhook)
  ↓ stores consentRequest.id on AbdmConsent

Step 2: Patient Approves/Denies in ABHA App
  ↓
  ABDM → POST /integration/abdm/api/v3/hiu/consent/request/notify (webhook)
  ↓ { notification: { status: "GRANTED", consentArtefacts: [{id: "..."}] } }
  Backend updates consent status + stores artefact ID
  Backend → POST /api/hiecm/consent/v3/request/hiu/on-notify (ACK)

Step 3: Fetch Consent Artefact (HIU → ABDM)
  Backend → POST /api/hiecm/consent/v3/fetch { consentId }
  ↓
  ABDM → POST /integration/abdm/api/v3/hiu/consent/on-fetch (webhook)
  ↓ returns signed artefact details

Step 4: Request Health Data (HIU → ABDM)
  Backend → POST /api/hiecm/data-flow/v3/health-information/request { consentId }
  ↓
  ABDM → POST /integration/abdm/api/v3/hiu/health-information/on-request (webhook)
  ↓ returns transactionId
  ABDM → POST /integration/abdm/api/v3/hip/health-information/request (to HIP)
  ↓ HIP transfers data to HIU's dataPushUrl
  ABDM → POST /api/hiecm/data-flow/v3/health-information/notify (sessionStatus: TRANSFERRED)

Step 1: Create consent request via CMS:

  1. Patient Detail → ABDM tab → Consents subtab
  2. Click "New Consent Request"
  3. Select ABHA address (picker shows all linked ABHAs)
  4. Fill: Purpose, HI types, date range, expiry
  5. Submit

OR via curl:

curl -X POST http://localhost:3000/api/integration/abdm/consents \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{
    "patient_id": "019f82e1-3d73-7a4a-88c8-da423b257e53",
    "abha_address": "simmi@sbx",
    "purpose": "care_management",
    "hi_types": ["OPConsultation", "Prescription"],
    "date_range_from": "2026-06-01T00:00:00.000Z",
    "date_range_to": "2026-07-28T00:00:00.000Z",
    "data_erase_at": "2026-08-28T00:00:00.000Z"
  }'

Step 2: Check on-init callback arrived:

cd backend && bundle exec rails runner "
ActiveRecord::Base.connection.execute('SET search_path TO public, integration')
c = Integration::AbdmConsent.where(direction: 'hiu').order(created_at: :desc).first
puts \"id=\#{c.id} status=\#{c.status} consent_request_id=\#{c.consent_request_id}\"
puts \"metadata=\#{c.metadata.inspect[0..200]}\"
"

Expected: status=requested, consent_request_id is populated (not nil)

Step 3: Patient grants in ABHA app:

  1. Open ABHA app (or PHR sandbox)
  2. Go to consent requests
  3. Find the request from IN1810000370
  4. Click Approve

Step 4: Verify GRANTED callback:

cd backend && bundle exec rails runner "
ActiveRecord::Base.connection.execute('SET search_path TO public, integration')
c = Integration::AbdmConsent.where(direction: 'hiu').order(created_at: :desc).first
puts \"status=\#{c.status}\"
puts \"consent_id=\#{c.consent_id.inspect}\"

# Check artefacts
Integration::AbdmConsentArtefact.where(abdm_consent_id: c.id).each do |a|
  puts \"artefact=\#{a.id} fetch_status=\#{a.fetch_status} artefact_id=\#{a.artefact_id}\"
end
"

Expected: status=granted, artefact created with fetch_status=pending

Step 5: Verify data fetch triggered: After GRANTED, the backend auto-fetches the consent artefact, then requests health data. Check the API logs:

cd backend && bundle exec rails runner "
ActiveRecord::Base.connection.execute('SET search_path TO public, integration')
Integration::AbdmApiLog.where('created_at > ?', 10.minutes.ago).order(created_at: :desc).each do |l|
  puts \"\#{l.created_at.strftime('%H:%M:%S')} | \#{l.url.split('/').last(3).join('/')} | \#{l.response_status} | ok=\#{l.success}\"
end
"

Expected sequence:

  • consent/v3/fetch → 202
  • data-flow/v3/health-information/request → 202
  • HIP data request callback → HIP provides data
  • Data notification → sessionStatus: TRANSFERRED
  1. Create a new consent request (same as Step 1 above)
  2. In ABHA app, click Deny
  3. Verify:
cd backend && bundle exec rails runner "
ActiveRecord::Base.connection.execute('SET search_path TO public, integration')
c = Integration::AbdmConsent.where(direction: 'hiu').order(created_at: :desc).first
puts \"status=\#{c.status}\"  # Expected: denied
"
  1. Create a consent request with a SHORT expiry (e.g. 5 minutes from now):
curl -X POST http://localhost:3000/api/integration/abdm/consents \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{
    "patient_id": "019f82e1-3d73-7a4a-88c8-da423b257e53",
    "abha_address": "simmi@sbx",
    "purpose": "care_management",
    "hi_types": ["OPConsultation"],
    "date_range_from": "2026-06-01T00:00:00.000Z",
    "date_range_to": "2026-07-28T00:00:00.000Z",
    "data_erase_at": "'$(date -u -v+5M +"%Y-%m-%dT%H:%M:%S.000Z")'"
  }'
  1. Don't approve in ABHA app — let it expire
  2. Wait 5+ minutes
  3. ABDM sends EXPIRED notification
  4. Verify: status=expired
  1. First get a GRANTED consent (Test A)
  2. In ABHA app, go to active consents → Revoke
  3. ABDM sends REVOKED notification to both HIU and HIP
  4. Verify:
cd backend && bundle exec rails runner "
ActiveRecord::Base.connection.execute('SET search_path TO public, integration')
c = Integration::AbdmConsent.where(direction: 'hiu').order(created_at: :desc).first
puts \"status=\#{c.status}\"  # Expected: revoked
"

Monitoring Callbacks

All inbound ABDM webhooks are logged. Watch them in real-time:

# Tail the Rails log for ABDM callbacks
tail -f backend/log/development.log | grep -i "abdm\|callback\|webhook\|consent\|link"

# Check stored callbacks
cd backend && bundle exec rails runner "
ActiveRecord::Base.connection.execute('SET search_path TO public, integration')
Integration::AbdmCallback.order(created_at: :desc).limit(10).each do |cb|
  puts \"\#{cb.created_at.strftime('%H:%M:%S')} | \#{cb.event_type} | \#{cb.payload.inspect[0..200]}\"
end
"

Key swagger references

FlowSpec file
M2 Link tokendocs/abdm/specs/M2/link-token.yaml
M2 Care context linkingdocs/abdm/specs/M2/hip-initiated-linking.yaml
M3 Consent init/notify/fetchdocs/abdm/specs/M3/consent-management-data-flow.yaml
M3 Data flow (health-info)docs/abdm/specs/M3/consent-management-data-flow.yaml (data-flow paths)

Key route fixes applied (2026-07-28)

RoutePurpose
POST /api/v3/link/on_carecontextM2: care context linking callback
POST /api/v3/hip/health-information/requestM3: HIP data request from ABDM
POST /api/v3/consent/request/hip/notifyM3: HIP consent notification
POST /api/v3/hiu/consent/on-fetchM3: HIU consent artefact fetch callback
POST /api/v3/data-flow/health-information/notifyM3: data transfer notification
ACK body fixacknowledgement → array with consentId (not consentRequestId)
Frequency fixvalue: 0 → value: 1 (swagger requires ≥ 1)

On this page