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 appStep 1: Verify ABHA + Auto Link Token
Via CMS UI:
- Go to Patient Detail → ABDM tab → Profile subtab
- Click "Setup ABHA" → "Register new ABHA" (or search existing)
- Enter ABHA address (e.g.
simmi@sbx), verify via OTP - 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
Step 2: Link Care Contexts
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.
M3: Consent Management + Data Flow
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)Test A: Consent Approval (GRANTED)
Step 1: Create consent request via CMS:
- Patient Detail → ABDM tab → Consents subtab
- Click "New Consent Request"
- Select ABHA address (picker shows all linked ABHAs)
- Fill: Purpose, HI types, date range, expiry
- 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:
- Open ABHA app (or PHR sandbox)
- Go to consent requests
- Find the request from
IN1810000370 - 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→ 202data-flow/v3/health-information/request→ 202- HIP data request callback → HIP provides data
- Data notification →
sessionStatus: TRANSFERRED
Test B: Consent Denial (DENIED)
- Create a new consent request (same as Step 1 above)
- In ABHA app, click Deny
- 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
"Test C: Consent Expiry (EXPIRED)
- 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")'"
}'- Don't approve in ABHA app — let it expire
- Wait 5+ minutes
- ABDM sends EXPIRED notification
- Verify:
status=expired
Test D: Consent Revocation (REVOKED)
- First get a GRANTED consent (Test A)
- In ABHA app, go to active consents → Revoke
- ABDM sends REVOKED notification to both HIU and HIP
- 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
| Flow | Spec file |
|---|---|
| M2 Link token | docs/abdm/specs/M2/link-token.yaml |
| M2 Care context linking | docs/abdm/specs/M2/hip-initiated-linking.yaml |
| M3 Consent init/notify/fetch | docs/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)
| Route | Purpose |
|---|---|
POST /api/v3/link/on_carecontext | M2: care context linking callback |
POST /api/v3/hip/health-information/request | M3: HIP data request from ABDM |
POST /api/v3/consent/request/hip/notify | M3: HIP consent notification |
POST /api/v3/hiu/consent/on-fetch | M3: HIU consent artefact fetch callback |
POST /api/v3/data-flow/health-information/notify | M3: data transfer notification |
| ACK body fix | acknowledgement → array with consentId (not consentRequestId) |
| Frequency fix | value: 0 → value: 1 (swagger requires ≥ 1) |