Kulcare Docs

M2 + M3 Comprehensive Testing Guide

Date: 2026-07-16 Branch: feature/abdm_integration_port (UNCOMMITTED — DO NOT COMMIT until you finish testing) Scope: M2 (user-initiated linking, data flow, automatic notify flows) + M3 (HIU consent + data fetch)


0. What You've Already Tested

✅ M2 — HIP-Initiated Linking (the generate_link_token flow from m1-abha-verification.md)

  • POST /integration/abdm/abha/verifications/generate_link_token
  • ABDM webhook /integration/abdm/api/v3/hip/token/on-generate-token
  • Ably event abdm.link_token_generated
  • GET /integration/abdm/abha/verifications/link_status
  • abdm_api_logs and abdm_callbacks entries

✅ M1 — ABHA Verification (all 6 identifier×channel flows)


1. What This Guide Covers

This guide walks through testing every flow that has NOT been tested yet:

SectionFlowTools Needed
§4M2.2 — User-Initiated Linking (full PHR app flow)Postman + PHR Sandbox app
§5M2.3 — Encounter finalization → care context linking → auto-notify flows (U6/U7)Postman + manual encounter finalization
§6M2.4 — Data Flow (HIP side) — consent notify + data request + FHIR bundlePostman + HIU consent (cross-flow with M3)
§7M3 — Full HIU consent lifecycle + data fetchPostman + PHR Sandbox app

2. Test Environment Setup

2.1 Backend Services Required

cd backend

# Required services
redis-server                                        # Redis
postgres -D /usr/local/var/postgres                 # PostgreSQL
elasticsearch                                       # Elasticsearch (search)

# Background workers — MULTIPLE queues needed:
bundle exec sidekiq                                # Default queues + abdm

# Server
bundle exec rails server -p 6000                    # Backend on :6000

⚠️ Important: Restart rails server AND sidekiq after pulling these changes (new files: care_context_notify_service.rb, sms_notify_service.rb, updated handlers).

2.2 Frontend (for E2E browser testing — optional)

cd frontend && pnpm dev
# CMS:   http://localhost:5174  (or check frontend/apps/cms/package.json)
# Patient: http://localhost:5173  (for PHR app integrations)

2.3 Public Webhook Tunnel

ABDM sandbox sends webhooks to YOUR backend. If your backend is on localhost:6000, ABDM can't reach it without a tunnel.

Start ngrok (or your preferred tunnel):

ngrok http 6000
# Copy the HTTPS URL, e.g. https://abc123.ngrok-free.app

Inject into Rails (one-time per server boot, in rails console):

cd backend
bundle exec rails console
# Set callback URLs to your tunnel:
Shared::Setting.set('abdm_callback_base_url', 'https://abc123.ngrok-free.app')

# Verify:
Shared::Setting.get('abdm_callback_base_url')
# => "https://abc123.ngrok-free.app"

If the setting doesn't exist yet, check how callback_url is read in abdm/abha/registration_service.rb or hip/link_token_service.rb and create the setting there. If your test environment already hardcodes a callback URL, restart with the tunnel active.

2.4 ABHA Sandbox Test Credentials

ResourceValue
ABHA Addressjohndoe@sbx (rate-limited; create your own)
ABHA Addresssimmi2@sbx (used previously, may be rate-limited for OTP)
Mobile9876543210
Aadhaar<your-test-aadhaar> (use sandbox-recommended value, not real)
Sandbox basehttps://dev.abdm.gov.in
CM-IDsbx
Client-IDSBX_001751

⚠️ Rate limits: ABDM sandbox rate-limits aggressively. OTP ABDM-1027 blocks for 24h after 3-5 attempts. Use care.

2.5 Pre-loaded Test Data

ResourceID
Tenant01900000-0000-7000-8000-000000000001
Facility01900002-0000-7000-8000-000000000002
Practitioner01900003-0000-7000-8000-000000000001
Practitioner Role01900006-0000-7000-8000-000000000002
Patient (test)019da93a-25bf-7060-ab97-e45dddd97947 (abha_id: simmi2@sbx)

Why: U2 fixed JSON shape bugs in send_on_discover, send_link_on_init, send_link_on_confirm. These were previously sending patient: {} (object) instead of patient: [] (array), and missing response: { requestId }.

4.1 Flow Overview

   PHR Sandbox App                          Kulcare Backend
        │                                        │
        ├────[1] Patient searches for facility──►│  (no backend call;
        │                                        │   ABDM gateway handles
        │                                        │   patient↔HIP discovery)
        │                                        │
        │◄───[2] ABDM gateway routes discover────┤
        │   webhook to HIP                        │
        │                                        ├────[3] /on-discover
        │◄─────────(response: { requestId })────┤   response.requestId fix
        │                                        │
   [patient selects care contexts]               │
        │                                        │
        ├────[4] link_init webhook──────────────►│
        │                                        ├────[5] /on-init
        │◄─────────(response + OTP SMS)─────────┤   SMS via SmsService (U5)
        │                                        │
   [patient enters OTP in PHR app]               │
        │                                        │
        ├────[6] link_confirm webhook───────────►│
        │                                        ├────[7] /on-confirm
        │◄─────────(patient[]: array)───────────┤   patient array fix (U2)

4.2 Step-by-Step Test

Step 1-3: Discover Flow (PHR Sandbox App ↔ Kulcare)

  1. Open the ABDM Sandbox PHR app (or web equivalent). URL is in ABDM developer docs — typically https://phr.abdm.gov.in or sandbox variant.

  2. Log in to the PHR app with the test ABHA (simmi2@sbx or any of yours)

  3. In the PHR app, search for your facility — you'll need the HIP ID (X-HIP-ID) that was registered during M1.1 Register facility as HIP. Find it:

    cd backend
    bundle exec rails runner '
      puts Integration::AbdmFacilityRegistration.find_by(facility_id: "01900002-0000-7000-8000-000000000002")&.service_id
    '
  4. PHR app sends discover request → ABDM gateway → your backend webhook

Step 4: Verify Inbound Discover Webhook

Check abdm_callbacks:

cd backend
bundle exec rails runner '
  cb = Integration::AbdmCallback.where(callback_type: "hip.discovery.discover").order(created_at: :desc).first
  if cb
    puts "Callback ID: #{cb.id}"
    puts "Status: #{cb.status}"
    puts "Request ID: #{cb.request_id}"
    puts "Tenant ID: #{cb.tenant_id}"
  else
    puts "NO DISCOVER CALLBACK FOUND"
  end
'

Expected:

  • ✅ Callback exists with status processed
  • ✅ request_id is populated

Step 5: Verify Outbound send_on_discover (U2 fix)

Check abdm_api_logs immediately after the discover:

cd backend
bundle exec rails runner '
  log = Integration::AbdmApiLog.where(url: /on-discover/).order(created_at: :desc).first
  if log
    puts "URL: #{log.url}"
    puts "Status: #{log.response_status}"
    puts "Request payload:"
    puts JSON.pretty_generate(log.request_payload)
  end
'

Expected:

  • ✅ URL: https://dev.abdm.gov.in/api/hiecm/user-initiated-linking/v3/patient/care-context/on-discover (NOT /api/hiecm/... with no host)
  • ✅ Status: 200 or 202
  • ✅ Request payload includes:
    • response: { requestId: "<matching inbound requestId>" }
    • patient: [{ ... }] (ARRAY, not object)
    • matchedBy: ['ABHA_ADDRESS']
  1. In PHR app, select care contexts to link → PHR app sends link_init webhook

  2. Check abdm_api_logs for /on-init:

    cd backend
    bundle exec rails runner '
      log = Integration::AbdmApiLog.where(url: /on-init/).order(created_at: :desc).first
      puts "Status: #{log.response_status}"
      puts "Body: #{log.request_payload.except("response").to_json}"
    '

    Expected: response.requestId present.

  3. Check SMS delivery (U5):

    cd backend
    bundle exec rails runner '
      logs = Shared::NotificationLog.where(channel: "sms").order(created_at: :desc).limit(3)
      logs.each { |l| puts "#{l.recipient} | #{l.content[0..60]}... | #{l.status}" }
    '

    Expected: SMS with the OTP code sent to the patient's mobile (status sent or pending).

  4. Check AbdmDiscoveryRequest:

    cd backend
    bundle exec rails runner '
      d = Integration::AbdmDiscoveryRequest.order(created_at: :desc).first
      puts "Status: #{d.status}"
      puts "Verification code: #{d.verification_code}"
      puts "Patient: #{d.patient&.name}"
    '

    Expected: status: 'link_initiated', verification_code: 'XXXXXX'.

  5. PHR app shows OTP screen → patient enters OTP → sends link_confirm webhook

  1. PHR app sends link_confirm after patient enters OTP

  2. Check abdm_api_logs for /on-confirm:

    cd backend
    bundle exec rails runner '
      log = Integration::AbdmApiLog.where(url: /on-confirm/).order(created_at: :desc).first
      puts "Status: #{log.response_status}"
      puts "Body: #{log.request_payload.to_json}"
    '

    Expected:

    • ✅ URL correct
    • ✅ response: { requestId: ... }
    • ✅ patient: [{ ... }] (ARRAY)
  3. Verify linked care contexts:

    cd backend
    bundle exec rails runner '
      ctx = Integration::AbdmCareContext.where(discovery_request_id: Integration::AbdmDiscoveryRequest.order(:created_at).last.id)
      puts "Count: #{ctx.count}"
      ctx.each { |c| puts "#{c.care_context_reference} | #{c.sync_status}" }
    '

    Expected: care contexts linked with sync_status: 'linked'.

4.3 Pass Criteria for M2.2

  • ✅ Outbound calls have correct URLs (full https://dev.abdm.gov.in/...)
  • ✅ Outbound calls include response: { requestId }
  • ✅ Outbound calls have patient: [] arrays (where applicable)
  • ✅ SMS delivered to patient (visible in notification_logs)
  • ✅ AbdmDiscoveryRequest transitions through pending → matched → link_initiated → link_confirmed
  • ✅ Care contexts created with sync_status: 'linked'

5. M2.3 — Care Context Linking + Auto-Notify (U4, U6, U7)

Why: U4 wires FHIR bundles; U6 auto-fires context/notify; U7 auto-fires sms/notify. Verify all three auto-fires work after encounter finalization.

5.1 Pre-requisites

  • A patient with linked ABHA via M2.1 (or earlier session)
  • An encounter that can be "finalized" (transitioned to finished)
  • Active HIP registration

5.2 Step-by-Step Test

cd backend
bundle exec rails runner '
  p = Clinical::Patient.find("019da93a-25bf-7060-ab97-e45dddd97947")
  lr = Integration::AbdmLinkRequest.for_patient(p.id).order(created_at: :desc).first
  puts "Link request ID: #{lr&.id}"
  puts "Token present: #{lr&.link_token.present?}"
  puts "Token valid: #{lr&.token_valid?}"
  puts "Status: #{lr&.status}"
  puts "ABHA: #{lr&.abha_address}"
'

If token is expired/missing: call POST /integration/abdm/abha/verifications/generate_link_token (the flow you already tested).

Step 2: Create or Find an Encounter

If you have an in-progress encounter with prescriptions/observations already:

cd backend
bundle exec rails runner '
  e = Clinical::Encounter.where(patient_id: "019da93a-25bf-7060-ab97-e45dddd97947").order(created_at: :desc).first
  puts "Encounter: #{e.id}"
  puts "Status: #{e.status}"
  puts "Medications: #{e.medication_requests.count}"
  puts "Observations: #{e.observations.count}"
  puts "Created: #{e.created_at}"
'

If you don't have one, create via CMS UI: Add Appointment → Check-in → Encounter.

Step 3: Add Medications + Observations (if not already present)

Skip if encounter already has them.

Step 4: Trigger Care Context Linking (encounter finalization)

If encounter is still in_progress, transition to finished:

cd backend
bundle exec rails runner '
  e = Clinical::Encounter.where(patient_id: "019da93a-25bf-7060-ab97-e45dddd97947").order(created_at: :desc).first
  # Simulate the transition
  e.update!(status: "finished")
  Integration::Abdm::EnqueueCareContextLinkJob.perform_later(e.id)
'

Or via Postman if there's an encounter finalization endpoint (check clinical/encounters_controller.rb).

Step 5: Wait + Verify

Wait ~30 seconds for the job chain: EnqueueCareContextLinkJob → CareContextLinkingService#call → ABDM API → HipLinkHandler#on_add_contexts → auto-fires (SmsNotifyService) → ABDM webhook → handler

cd backend
bundle exec rails runner '
  # Latest link request
  lr = Integration::AbdmLinkRequest.order(created_at: :desc).first
  puts "Link request status: #{lr.status}"
  
  # Care contexts created
  ctx = Integration::AbdmCareContext.where(link_request: lr)
  puts "Care contexts: #{ctx.count}"
  
  # U4: FHIR bundles populated
  puts "FHIR bundles populated: #{ctx.where.not(fhir_bundle: nil).count} of #{ctx.count}"
  ctx.each do |c|
    bundle_size = c.fhir_bundle.is_a?(Hash) ? c.fhir_bundle["entry"]&.size : "nil"
    puts "  #{c.care_context_reference} | #{c.resource_type} | fhir entries: #{bundle_size} | sync_status: #{c.sync_status}"
  end
'

Expected:

  • ✅ link_request.status: 'completed'
  • ✅ Care contexts exist (1 OPConsultation + N prescriptions + M observations)
  • ✅ U4 fix: fhir_bundle is NOT null for OPConsultation + Prescription contexts (DiagnosticReport still nil — builder doesn't exist yet)
  • ✅ sync_status: 'linked'

Step 6: Verify U6 Auto-Fire (Care Context Notify)

Check outbound abdm_api_logs:

cd backend
bundle exec rails runner '
  logs = Integration::AbdmApiLog.where(url: /link\/context\/notify/).order(created_at: :desc).limit(5)
  puts "Count: #{logs.count}"
  logs.each do |l|
    puts "#{l.http_method} #{l.response_status} #{l.url}"
    puts "  payload keys: #{l.request_payload.keys}"
    puts "  patient count: #{l.request_payload["patient"]&.size}"
    puts "  hiTypes: #{l.request_payload["hiTypes"]}"
  end
'

Expected: 1+ outbound calls to /link/context/notify, status 200/202.

Check U6 inbound webhook (hip.context_notify):

cd backend
bundle exec rails runner '
  cb = Integration::AbdmCallback.where(callback_type: "hip.context_notify").order(created_at: :desc).first
  if cb
    puts "Callback status: #{cb.status}"
    puts "Request ID: #{cb.request_id}"
  else
    puts "NO CONTEXT_NOTIFY CALLBACK (ABDM may not have one queued yet)"
  end
'

Check AbdmCareContext.metadata:

cd backend
bundle exec rails runner '
  c = Integration::AbdmCareContext.where.not(metadata: nil).order(created_at: :desc).first
  puts "Metadata keys: #{c.metadata.keys}"
  puts "last_notify_at: #{c.metadata["last_notify_at"]}"
  puts "last_notify_request_id: #{c.metadata["last_notify_request_id"]}"
'

Expected: last_notify_at + last_notify_request_id populated by the inbound webhook.

Step 7: Verify U7 Auto-Fire (SMS Notify)

Check outbound abdm_api_logs:

cd backend
bundle exec rails runner '
  logs = Integration::AbdmApiLog.where(url: /sms\/notify2/).order(created_at: :desc).limit(5)
  puts "Count: #{logs.count}"
  logs.each do |l|
    puts "#{l.response_status} #{l.url}"
    puts "  payload: #{l.request_payload.to_json}"
  end
'

Expected: 1+ outbound calls to /sms/notify2, status 200/202.

Check AbdmLinkRequest.metadata:

cd backend
bundle exec rails runner '
  lr = Integration::AbdmLinkRequest.order(created_at: :desc).first
  puts "last_sms_notify_at: #{lr.metadata&.dig("last_sms_notify_at")}"
  puts "last_sms_notify_request_id: #{lr.metadata&.dig("last_sms_notify_request_id")}"
'

Check U7 inbound webhook (hip.sms_notify):

cd backend
bundle exec rails runner '
  cb = Integration::AbdmCallback.where(callback_type: "hip.sms_notify").order(created_at: :desc).first
  puts cb ? "Callback: #{cb.status}" : "NO SMS_NOTIFY CALLBACK"
'

5.3 Pass Criteria for M2.3

  • ✅ Encounter finalization triggers care context linking (already worked, but verify)
  • ✅ U4: fhir_bundle populated on OPConsultation + Prescription care contexts (not nil)
  • ✅ U6: Auto-fired outbound call to /link/context/notify recorded in abdm_api_logs
  • ✅ U6: Inbound hip.context_notify webhook received and processed
  • ✅ U7: Auto-fired outbound call to /sms/notify2 recorded in abdm_api_logs
  • ✅ U7: Inbound hip.sms_notify webhook received and processed

6. M2.4 + Cross-Test with M3 — Data Flow (HIP side)

Why: U1 fixed DataTransferService#gateway_url. U3 added HIP consent ACK. Verify the full data flow works end-to-end.

This section overlaps with §7 (M3) — the M3 "fetch data" step is what triggers the M2 "push data" step. Test them together.

6.1 Pre-requisites

  • Patient has been linked via M2 (any flow)
  • HIU is registered (M1.2 in Postman collection — needed before §7 M3)

6.2 Test Steps

Step 1: Get Your HIU ID

cd backend
bundle exec rails runner '
  reg = Integration::AbdmFacilityRegistration.find_by(facility_id: "01900002-0000-7000-8000-000000000002", registration_type: "HIU")
  puts reg&.service_id
'

Use Postman M3.1 Create HIU consent request with body:

{
  "patient_id": "019da93a-25bf-7060-ab97-e45dddd97947",
  "requester_id": "<HIU_ID_FROM_STEP_1>",
  "requester_type": "HIU",
  "purpose": "care_management",
  "hi_types": ["Prescription", "DiagnosticReport"],
  "date_range_from": "2024-01-01",
  "date_range_to": "2026-12-31",
  "data_erase_at": "2026-12-31T23:59:59Z"
}

Capture consent_id (auto-saved by Postman test script).

Step 3: Patient Approves in PHR App

  1. Patient logs into PHR Sandbox app
  2. Notification: "Facility X requests your records"
  3. Patient clicks Approve

Step 4: Verify Webhook Inbound

cd backend
bundle exec rails runner '
  cb = Integration::AbdmCallback.where(callback_type: "hip.consent_notify").order(created_at: :desc).first
  puts cb ? "Status: #{cb.status} | Request ID: #{cb.request_id}" : "NO CONSENT_NOTIFY CALLBACK"
  
  consent = Integration::AbdmConsent.order(created_at: :desc).first
  puts "Consent status: #{consent.status}"
  puts "Consent ID (ABDM): #{consent.consent_id}"
  puts "Consent expiry: #{consent.consent_expiry}"
'

Expected:

  • ✅ Callback status: 'processed'
  • ✅ Consent record exists with status: 'granted'
cd backend
bundle exec rails runner '
  log = Integration::AbdmApiLog.where(url: /hip\/on-notify/).order(created_at: :desc).first
  puts "URL: #{log.url}"
  puts "Status: #{log.response_status}"
  puts "Payload: #{log.request_payload.to_json}"
'

Expected:

  • ✅ URL: https://dev.abdm.gov.in/api/hiecm/consent/v3/request/hip/on-notify
  • ✅ Payload includes acknowledgement: { status: "OK" } + response: { requestId }
  • ✅ Status: 200/202

Step 6: Continue with M3 §7 — fetch data

(Now in §7 below — proceed to M3.5+)


See /Users/simmibadhan/Projects/Kulcare/kulcare-platform/docs/abdm/api-reference/m3-consent-management.md for full API reference. This section gives you the testing sequence.

7.1 Pre-requisites (Re-Checked)

  • ✅ Patient has linked ABHA (from M1/M2)
  • ✅ HIU registered (Postman M1.2)
  • ✅ CM-ID and gateway credentials set
  • ✅ Backend rails server and sidekiq running
  • ✅ abdm_callbacks queue active (verify with Sidekiq::Queue.new("abdm_callbacks").size)

7.2 Test Sequence

Use Postman. Body as in §6.2 Step 2.

Verify:

cd backend
bundle exec rails runner '
  c = Integration::AbdmConsent.order(created_at: :desc).first
  puts "Consent_request_id (ABDM): #{c.consent_request_id}"
  puts "Status: #{c.status}"
  puts "Expires: #{c.consent_expiry}"
'

Expected:

  • ✅ consent_request_id populated (U2 fix — was nil before)
  • ✅ Status: requested or similar
  1. Open PHR Sandbox app on phone/web
  2. Log in as patient
  3. Notifications → "Consent Request from Facility X"
  4. Review what data is being requested
  5. Approve

Verify inbound webhook:

cd backend
bundle exec rails runner '
  cb = Integration::AbdmCallback.where(callback_type: "hiu.consent_notify").order(created_at: :desc).first
  puts cb ? "Callback: #{cb.status}" : "NO HIU CONSENT_NOTIFY CALLBACK"
  
  c = Integration::AbdmConsent.order(created_at: :desc).first
  puts "Local status: #{c.status}"
  puts "consent_request_id: #{c.consent_request_id}"
'

Expected:

  • ✅ Callback status: 'processed'
  • ✅ Local consent status updates (e.g., granted)
  • ✅ consent_request_id STILL present (U2 fix was about persistence)

Step 3: Verify HIU on-notify ACK (U3 M3 fix)

cd backend
bundle exec rails runner '
  log = Integration::AbdmApiLog.where(url: /hiu\/on-notify/).order(created_at: :desc).first
  puts "URL: #{log.url}"
  puts "Status: #{log.response_status}"
  puts "Payload: #{log.request_payload.to_json}"
'

Expected: ACK outbound to /consent/v3/request/hiu/on-notify.

Step 4: Verify Ably Event abdm.consent_status_changed

cd backend
bundle exec rails runner '
  c = Integration::AbdmConsent.order(created_at: :desc).first
  puts "requested_by_id: #{c.requested_by_id}"
  puts "requested_by_type: #{c.requested_by_type}"
'

Then subscribe to the Ably channel private:user:<requested_by_id> from your FE/dev tools and verify a abdm.consent_status_changed event was published.

If you don't have an Ably client: skip this verification — the publish is fire-and-forget with logging.

  • GET /integration/abdm/consents?patient_id=... — verify consent appears
  • GET /integration/abdm/consents/:id — verify detailed view

GET /integration/abdm/cm/artefacts?consent_id=...

Expected: Empty or [] initially (no data fetched yet).

Step 7: Trigger Data Fetch from HIU

This is the M3.5-equivalent on the data side. In a real HIU, you'd call ABDM's data-flow endpoint to fetch the consented records. For testing, you can either:

Option A (simulate HIU request):

cd backend
bundle exec rails runner '
  # Simulate an HIU data request via the same backend path
  consent = Integration::AbdmConsent.find_by(status: "granted")
  # The HIU would normally call:
  # POST /api/hiecm/data-flow/v3/health-information/request
  # which causes ABDM to fire /on_request webhook at the HIP
  puts "Manual trigger: simulate by calling DataFetchService"
'

Option B: Use Postman M3 endpoints if there are "request data" ones.

For sandbox testing without an actual HIU client app, you may need to:

  1. Construct an ABDM data request manually (via Postman to ABDM with HIU creds)
  2. Wait for ABDM to send the hip_data_request webhook to your backend
  3. Verify your backend:
    • Creates AbdmDataTransfer record
    • Sends HIP on-request ACK (U1 fix)
    • Acknowledges data
    • Pushes encrypted FHIR bundles to HIU
    • Sends data-flow notify
cd backend
bundle exec rails runner '
  dt = Integration::AbdmDataTransfer.order(created_at: :desc).first
  if dt
    puts "Data transfer: #{dt.status}"
    puts "Transaction: #{dt.transaction_id}"
    puts "Consent: #{dt.abdm_consent_id}"
  else
    puts "NO DATA TRANSFER (ABDM data flow not triggered)"
  end
'

Step 8: M3.8 Show Artefact (Postman)

After data is fetched, GET /integration/abdm/cm/artefacts/:id should return the artefact with fhir_records populated.

7.3 Pass Criteria for M3

  • ✅ Consent request creates record with consent_request_id (U2)
  • ✅ Patient approval triggers inbound hiu.consent_notify webhook
  • ✅ HIP consent ACK + HIU consent ACK sent (U3 M2 + U3 M3)
  • ✅ Consent status updates to granted
  • ✅ Ably event abdm.consent_status_changed published
  • ✅ M3.9 GET status endpoint returns latest status from ABDM
  • ✅ Optional: Data fetch triggers the full data flow

8. Quick Debugging Tips

8.1 Most Common Issues

SymptomLikely CauseFix
abdm_api_logs shows URL without hostU1 regression — .dig(:abdm, ...)Check credential access pattern
ABDM rejects patient array as schema mismatchU2 regression — patient: {} (object)Wrap in []
Webhook returns 200 but ABDM times outMissing response: { requestId }Add envelope
SMS not deliveredSmsService failed silentlyCheck notification_logs for failure reason
FHIR bundle nil in data transferU4 regressionCheck FHIR builder associations match encounter shape
404 Not Found on /api/v3/.../on-notifyRoute missingVerify routes file has the new entries
No Ably event firedAbly disabled in test envCheck Notifications::Realtime.enabled?

8.2 Useful Queries

# All recent ABDM logs
cd backend
bundle exec rails runner '
  Integration::AbdmApiLog.order(created_at: :desc).limit(20).each do |l|
    puts "#{l.created_at} | #{l.http_method} #{l.response_status} | #{l.url[0..80]}"
  end
'

# All recent ABDM callbacks (webhooks)
cd backend
bundle exec rails runner '
  Integration::AbdmCallback.order(created_at: :desc).limit(20).each do |c|
    puts "#{c.created_at} | #{c.callback_type} | #{c.status} | retry: #{c.retry_count}"
  end
'

# Failed webhooks (retry > 0)
cd backend
bundle exec rails runner '
  Integration::AbdmCallback.where("retry_count > 0 OR status = ?", "failed").order(created_at: :desc).limit(10).each do |c|
    puts "#{c.callback_type} | #{c.status} | #{c.error_code} | #{c.error_message}"
  end
'

# Sidekiq queue health
cd backend
bundle exec rails runner '
  puts "abdm queue: #{Sidekiq::Queue.new("abdm").size}"
  puts "abdm_callbacks queue: #{Sidekiq::Queue.new("abdm_callbacks").size}"
  puts "abdm failed: #{Sidekiq::DeadSet.new.size}"
'

8.3 When Something Goes Wrong

  1. Capture exactly what failed: postman response body + server log line
  2. Check abdm_callbacks queue: Sidekiq::Queue.new("abdm_callbacks").size — if growing, sidekiq may be down
  3. Check Sidekiq: tail -f log/sidekiq.log
  4. Re-fire a specific callback:
    cd backend
    bundle exec rails runner '
      cb = Integration::AbdmCallback.find(<id>)
      cb.update!(status: "received")  # Reset
      Integration::Abdm::CallbackProcessor.new(cb).process!
    '

9. Test Result Tracker

Use this checklist as you go:

M2.2 — User-Initiated Linking (PHR app)

  • Discover webhook arrives
  • /on-discover outbound has response.requestId + patient: []
  • Link_init webhook arrives
  • /on-init outbound has response.requestId
  • SMS delivered to patient
  • Link_confirm webhook arrives
  • /on-confirm outbound has response.requestId + patient: []
  • Care contexts linked

M2.3 — Care Context + Auto-Notify

  • Care contexts created with fhir_bundle populated (U4)
  • Link request transitions to completed
  • /link/context/notify outbound fires (U6)
  • /api/v3/links/context/on-notify inbound received (U6)
  • /sms/notify2 outbound fires (U7)
  • /api/v3/patients/sms/on-notify inbound received (U7)

M2.4 / M3 — Data Flow

  • Consent request creates with consent_request_id populated (U2-M3)
  • Patient approves in PHR → inbound hiu.consent_notify
  • HIU on-notify ACK outbound (U3-M3)
  • Consent status updates to granted
  • Ably event abdm.consent_status_changed fires
  • (Optional) HIU data request triggers hip_data_request webhook
  • (Optional) DataTransferService encrypts + pushes + notifies
  • (Optional) FHIR bundles in transfer NOT empty (U4)

10. When You're Done

Once all test results are filled in, let me know:

  1. All green? → I'll help commit the M2 + M3 changes
  2. Some red? → I'll help debug + fix
  3. Questions about flow? → Ask before testing that section

The original plan (docs/plans/2026-07-10-003-feat-abdm-m2-gap-fixes-plan.md) stays in the working tree for reference.

On this page

0. What You've Already Tested1. What This Guide Covers2. Test Environment Setup2.1 Backend Services Required2.2 Frontend (for E2E browser testing — optional)2.3 Public Webhook Tunnel2.4 ABHA Sandbox Test Credentials2.5 Pre-loaded Test Data4.1 Flow Overview4.2 Step-by-Step TestStep 1-3: Discover Flow (PHR Sandbox App ↔ Kulcare)Step 4: Verify Inbound Discover WebhookStep 5: Verify Outbound send_on_discover (U2 fix)Step 6-7: Link_Init FlowStep 8-9: Link_Confirm Flow4.3 Pass Criteria for M2.25. M2.3 — Care Context Linking + Auto-Notify (U4, U6, U7)5.1 Pre-requisites5.2 Step-by-Step TestStep 1: Verify Patient Has Valid Link TokenStep 2: Create or Find an EncounterStep 3: Add Medications + Observations (if not already present)Step 4: Trigger Care Context Linking (encounter finalization)Step 5: Wait + VerifyStep 6: Verify U6 Auto-Fire (Care Context Notify)Step 7: Verify U7 Auto-Fire (SMS Notify)5.3 Pass Criteria for M2.36. M2.4 + Cross-Test with M3 — Data Flow (HIP side)6.1 Pre-requisites6.2 Test StepsStep 1: Get Your HIU IDStep 2: Create a Consent Request from HIUStep 3: Patient Approves in PHR AppStep 4: Verify Webhook InboundStep 5: Verify U3 Auto-Fire (HIP Consent ACK)Step 6: Continue with M3 §7 — fetch data7. M3 — HIU Consent Management Full Lifecycle7.1 Pre-requisites (Re-Checked)7.2 Test SequenceStep 1: M3.1 Create HIU Consent Request (Postman)Step 2: M3.2 Approve Consent (Patient in PHR App)Step 3: Verify HIU on-notify ACK (U3 M3 fix)Step 4: Verify Ably Event abdm.consent_status_changedStep 5: M3.5-M3.6 List + Show Consent (Postman)Step 6: M3.7 List Consent Artefacts (Postman)Step 7: Trigger Data Fetch from HIUStep 8: M3.8 Show Artefact (Postman)7.3 Pass Criteria for M38. Quick Debugging Tips8.1 Most Common Issues8.2 Useful Queries8.3 When Something Goes Wrong9. Test Result TrackerM2.2 — User-Initiated Linking (PHR app)M2.3 — Care Context + Auto-NotifyM2.4 / M3 — Data Flow10. When You're Done