ABDM Consent Management & Health Information Exchange (M2/M3)

What this document covers

This document explains how orvo-abha exchanges a patient's health records with other systems in India's ABDM (Ayushman Bharat Digital Mission) network, under the patient's explicit consent. In plain terms: a hospital or clinic ("HIP" — Health Information Provider) holds a patient's records; another system ("HIU" — Health Information User, e.g. a different hospital or the patient's own PHR app) wants to read those records; ABDM's gateway brokers a consent request to the patient, and once the patient approves it on their Consent Manager app, the actual encrypted medical data (FHIR bundles) flows from the HIP to the HIU. Everything here is asynchronous — every step is a POST that gets acknowledged immediately (202), with the real result delivered later via a separate inbound callback from ABDM.

This single orvo-abha codebase plays both roles simultaneously, because it hosts many different facilities:

Every consent request moves through one of these states, stored in the Consent table's status column:

REQUESTED → GRANTED → (REVOKED | EXPIRED)
         ↘ DENIED

ABDM's compliance rule is blunt: once a consent goes REVOKED or EXPIRED, every copy of data pulled in under it must be deleted, not just have its status flag flipped. This codebase enforces that in one place (updateConsentStatus in consent-callback.operations.ts) — see the Purge on revoke/expiry box under Health Information Request & Transfer.

Everything below is grounded in the actual route files, Zod schemas, and service/operation code under:

Mount points, confirmed from src/app.ts:

Router Mounted at Rate limiter
consentRouter /v1/consents, /v2/consents writeLimiter
healthDocumentRouter /v1/health-documents writeLimiter
lhrRouter /v1/phr/lhr, /v2/phr/lhr generalLimiter
hiuSubscriptionRouter /v1/hiu-subscription generalLimiter
consentCallbackRouter / (root — ABDM calls fixed absolute paths) none
subscriptionCallbackRouter / (root) none

Outbound-only note: consent.routes.ts itself applies no authentication middleware (no requireXHipId, no requireAuth) — it's called by orvo-hub over the Orvo session cookie, and facility scoping happens by an optional hipId query/body parameter that falls back to "the single active facility" if omitted (see resolveFacilityForConsent). This is a single-tenant-per-deployment assumption baked into the consent module; health-document and hiu-subscription routers, by contrast, do enforce X-HIP-ID at the router level.


This is the side where a facility on this system asks ABDM, on the patient's behalf, for permission to pull in health records held somewhere else (another hospital, or — for a patient using the PHR app — a "self-fetch" of the patient's own records).

GET /v1/consents

Lists consent requests this facility has made as an HIU (excludes patient self-fetch/PATRQT requests, which are a separate, auto-driven flow not meant for the operator's manual queue).

{
  "success": true,
  "message": "Success",
  "data": [
    {
      "id": "1c2b3a4d-...",
      "consentId": "a9f0e6b0-6e2f-4b9b-9f0a-1a2b3c4d5e6f",
      "abhaAddress": "ravi.kumar@sbx",
      "hiuId": "IN0310000702_1",
      "status": "GRANTED",
      "purposeCode": "CAREMGT",
      "hiTypes": ["DIAGNOSTICREPORT", "PRESCRIPTION"],
      "dataDateRangeFrom": "2024-01-01T00:00:00.000Z",
      "dataDateRangeTo": "2026-01-01T00:00:00.000Z",
      "patientName": "Ravi Kumar"
    }
  ],
  "requestId": "..."
}

POST /v1/consents/consent/init

Submits a raw ABDM-shaped consent request. This is the low-level wrapper — it accepts exactly the consent object ABDM's own consent/v3/request/init endpoint expects, for callers who want full control (rather than the simplified /create wrapper below).

{
  "consent": {
    "purpose": { "text": "Care Management", "code": "CAREMGT" },
    "patient": { "id": "ravi.kumar@sbx" },
    "hip": { "id": "IN0310000741_1", "name": "City Hospital", "type": "HIP" },
    "hiu": { "id": "IN0310000702_1", "name": "Orvo Technologies Private Limited", "type": "HIU" },
    "hiTypes": ["DiagnosticReport", "Prescription"],
    "permission": {
      "accessMode": "VIEW",
      "dateRange": { "from": "2024-01-01T00:00:00.000Z", "to": "2026-01-01T00:00:00.000Z" },
      "dataEraseAt": "2026-04-01T00:00:00.000Z",
      "frequency": { "unit": "DAY", "value": 1, "repeats": 0 }
    }
  }
}

POST /v1/consents/consent/status

Polls ABDM for the current status of an in-flight consent request.

POST /v1/consents/consent/fetch

Requests the full, signed consent artefact for a GRANTED consent (the exact HI types/date-range/care-contexts the patient actually approved — which can differ from what was originally requested, since the patient can edit the request before granting).

POST /v1/consents/create — simplified consent request

The friendly, all-in-one wrapper most callers actually use. It fills in sensible defaults (date ranges, HIU id, purpose normalization) and internally builds and submits the full ABDM consent/init payload.

{
  "abhaAddress": "ravi.kumar@sbx",
  "purpose": "Care Management",
  "dataTypes": ["DiagnosticReport"],
  "fromDate": "2024-01-01T00:00:00.000Z",
  "toDate": "2026-01-01T00:00:00.000Z",
  "hiuId": "IN0310000702_1",
  "hipId": "IN0310000741_1",
  "accessMode": "VIEW"
}
{
  "success": true,
  "message": "Consent request created",
  "data": {
    "requestId": "0f2a...",
    "consentId": "cons-9c1e6a70-1a2b3c4d5e6f",
    "status": "REQUESTED",
    "initiationStatus": "SUBMITTED",
    "gatewayResponse": { "requestId": "0f2a..." }
  }
}

GET /v1/consents/details/:requestId

Fetches a single consent record by its consentId. Returns 404 (wrapped in a 200-status success envelope with message: 'Consent not found') if missing.

GET /v1/consents/health-info, GET /v1/consents/data-pushes, GET /v1/consents/health-info/:transactionId/status, GET /v1/consents/health-info/:transactionId/records

Read-only monitoring endpoints for the data-flow (HIU) side — covered in detail under Health Information Request & Transfer below, since they're the query surface for that flow.


This is the side where another system (acting as HIU) requests data that lives in one of this system's facilities. Under the real ABDM design, the HIP itself never grants consent — only the patient does, via their Consent Manager app — the HIP's job is just to serve data once ABDM confirms a grant (see consent.hip-notify callback below). The endpoints in this section are a local operator convenience layer with an important caveat, documented directly in the code.

POST /v1/consents/:consentId/approve, POST /v1/consents/:consentId/deny, POST /v1/consents/:consentId/revoke

Operator actions on a consent request that's targeting this facility's data.

Caveat — read this before relying on /approve. ABDM has no gateway endpoint for a HIP to grant consent; consent is granted exclusively by the patient on the CM app. Calling this endpoint only flips the local Consent.status column to GRANTED — it does not call ABDM, does not produce a real ABDM consent artefact, and requestHealthInformationOperation (which requires a genuine abdmArtefactId) will not find one to act on. In effect, this only changes what the local dashboard displays; it is not a substitute for the patient's actual decision and does not unblock the real data flow. The genuine HIP-side approval path is the consent.hip-notify callback (below), which reflects ABDM's own record of what the patient decided. If the goal is to skip manual per-request review for a trusted HIU, use the ABDM-native auto-approval / subscription mechanism instead (next section, and see HIU Subscription).

GET /v1/consents/artifacts, GET /v1/consents/artifacts/:artefactId

Lists (or fetches one of) the GRANTED consent artefacts held for this facility as HIP — i.e., consents where some other HIU has been genuinely approved by the patient to pull this facility's data. Optional hipId query param scopes to a specific facility.

Auto-approval rules — GET/POST /v1/consents/auto/approve, PATCH /v1/consents/auto/approve/:ruleId/status, DELETE /v1/consents/auto/approve/:ruleId

Lets a facility pre-register a trusted HIU + set of HI types so that a new inbound REQUESTED consent (via consent.hip-notify) is auto-flipped to GRANTED locally without waiting on a human operator, if the rule matches.

{
  "hiuId": "IN0310000702_1",
  "hiuName": "Apollo Health City",
  "dataTypes": ["DIAGNOSTICREPORT", "PRESCRIPTION"],
  "expiryDays": 90
}

Same caveat as /approve above applies here too — this only flips the local flag when checkAndAutoApproveConsent runs during consent.hip-notify processing; it does not itself call ABDM.


Every path below is an Inbound ABDM callback — ABDM's gateway POSTs to it, not our own frontend. No Bearer/session auth is applied (ABDM is not a logged-in user); these routes are protected only by not being guessable/by the callback logger recording every hit for audit (abdmCallbackEvent). All controller handlers immediately respond 202 { "status": "accepted" } (via accepted(res)) and then process the payload asynchronously — ABDM's callback window is short, and processing (DB writes, decryption, Pusher events) must not hold up the HTTP response.

All the real, ABDM-facing paths carry the mandatory /api prefix (/api/v3/...) — this was reverse-engineered from real inbound traffic (abdmCallbackEvent.route) and is required, not optional.

POST /api/v3/hiu/consent/request/on-init — consent.on-init

ABDM acknowledges our consent/init submission and assigns its own consentRequest.id.

{
  "consentRequest": { "id": "b2f8b2b0-6e2f-4b9b-9f0a-1a2b3c4d5e6f" },
  "response": { "requestId": "0f2a-our-original-request-id" }
}

Or, on rejection:

{
  "error": { "code": "ABDM-9999", "message": "HIP and HIU cannot be the same entity" },
  "response": { "requestId": "0f2a-our-original-request-id" }
}

POST /api/v3/hiu/consent/request/on-status — consent.on-status

Delivers the answer to a consent/status poll.

{ "consentRequest": { "id": "b2f8b2b0-...", "status": "GRANTED" } }

POST /api/v3/hiu/consent/request/notify — consent.hiu-notify

The pivotal callback on the HIU side — ABDM tells us the patient's actual decision (approve/deny), and on later transitions, revoke/expiry too.

{
  "notification": {
    "consentRequestId": "b2f8b2b0-6e2f-4b9b-9f0a-1a2b3c4d5e6f",
    "status": "GRANTED",
    "consentArtefacts": [{ "id": "9c1e6a70-6e2f-4b9b-9f0a-1a2b3c4d5e6f" }]
  }
}

POST /api/v3/hiu/consent/on-fetch — consent.on-fetch

Delivers the full, ABDM-signed consent artefact after we called consent/fetch.

{
  "consent": {
    "id": "9c1e6a70-6e2f-4b9b-9f0a-1a2b3c4d5e6f",
    "consentDetail": {
      "patient": { "id": "ravi.kumar@sbx" },
      "hip": { "id": "IN0310000741_1", "name": "City Hospital" },
      "hiu": { "id": "IN0310000702_1" },
      "hiTypes": ["DiagnosticReport"],
      "careContexts": [
        { "patientReference": "ravi.kumar@sbx", "careContextReference": "ENC-2026-00123" }
      ],
      "permission": {
        "accessMode": "VIEW",
        "dateRange": { "from": "2024-01-01T00:00:00.000Z", "to": "2026-01-01T00:00:00.000Z" },
        "dataEraseAt": "2026-04-01T00:00:00.000Z"
      }
    },
    "signature": "eyJhbGciOiJSUzI1NiJ9..."
  }
}

POST /api/v3/consent/request/hip/on-notify — consent.hip-notify

ABDM tells this facility, acting as HIP, that a consent involving its data changed state (a third-party HIU was granted/revoked/expired access, or a brand-new third-party request just landed with REQUESTED).

{
  "notification": {
    "status": "GRANTED",
    "consentId": "9c1e6a70-6e2f-4b9b-9f0a-1a2b3c4d5e6f",
    "consentDetail": {
      "schemaVersion": "v3",
      "consentId": "9c1e6a70-6e2f-4b9b-9f0a-1a2b3c4d5e6f",
      "createdAt": "2026-07-18T10:00:00.000",
      "patient": { "id": "ravi.kumar@sbx" },
      "careContexts": [
        { "patientReference": "ravi.kumar@sbx", "careContextReference": "ENC-2026-00123" }
      ],
      "purpose": { "text": "Care Management", "code": "CAREMGT" },
      "hip": { "id": "IN0310000741_1", "name": "City Hospital" },
      "hiu": { "id": "IN0999999999_1", "name": "Some Other Hospital" },
      "consentManager": { "id": "sbx" },
      "hiTypes": ["DiagnosticReport"],
      "permission": {
        "accessMode": "VIEW",
        "dateRange": { "from": "...", "to": "..." },
        "dataEraseAt": "...",
        "frequency": { "unit": "DAY", "value": 1 }
      }
    },
    "signature": "eyJhbGciOiJSUzI1NiJ9...",
    "grantAcknowledgement": true
  }
}

Health Information Request & Transfer — the actual FHIR data movement

This is the heart of the system: once a consent is GRANTED, this is where the real, encrypted medical records move from the HIP that holds them to the HIU that requested them. All payloads here are end-to-end encrypted using ABDM's Fidelius scheme (ECDH key exchange on Curve25519 + AES-256-GCM), so ABDM's gateway itself never sees the plaintext.

POST /v1/consents/health-info/request (and the internal requestHealthInformationOperation)

Triggers the actual data pull for a GRANTED consent + artefact.

{
  "consentId": "cons-9c1e6a70-1a2b3c4d5e6f",
  "fromDate": "2025-01-01T00:00:00.000Z",
  "toDate": "2026-01-01T00:00:00.000Z",
  "hiTypes": ["DiagnosticReport"]
}
{
  "hiRequest": {
    "consent": { "id": "9c1e6a70-6e2f-4b9b-9f0a-1a2b3c4d5e6f" },
    "dateRange": { "from": "2025-01-01T00:00:00.000Z", "to": "2026-01-01T00:00:00.000Z" },
    "dataPushUrl": "https://abha.orvo.app/api/v3/hiu/health-information/on-request",
    "hiTypes": ["DiagnosticReport"],
    "keyMaterial": {
      "cryptoAlg": "ECDH",
      "curve": "Curve25519",
      "dhPublicKey": {
        "expiry": "2026-07-19T10:00:00.000Z",
        "parameters": "Curve25519/32byte random key",
        "keyValue": "base64-public-key..."
      },
      "nonce": "base64-32-random-bytes..."
    }
  }
}

POST /api/v3/hiu/health-information/on-request — the data-push callback, HIU side

Inbound ABDM callback — but not from ABDM's control plane; this is where the HIP itself (relayed through ABDM) POSTs the actual encrypted health records. This one URL carries two very different payload shapes:

1. Acknowledgement-only (no entries) — ABDM relaying the HIP's initial ack of our request:

{
  "response": { "requestId": "<our transactionId>" },
  "hiRequest": { "transactionId": "<ABDM's own transfer id>", "sessionStatus": "ACKNOWLEDGED" }
}

The handler captures hiRequest.transactionId (ABDM's transfer id, different from our own) so the real data push — tagged with that transfer id — can later be correlated back to our original request.

2. The actual encrypted data — realistic (abbreviated) shape per HipHealthInformationSchema:

{
  "transactionId": "abdm-transfer-id-...",
  "pageNumber": 0,
  "pageCount": 1,
  "entries": [
    {
      "content": "base64(ciphertext || 16-byte-authTag)...",
      "media": "application/fhir+json",
      "checksum": "5d41402abc4b2a76b9719d911017c592",
      "careContextReference": "ENC-2026-00123"
    }
  ],
  "keyMaterial": {
    "cryptoAlg": "ECDH",
    "curve": "Curve25519",
    "dhPublicKey": {
      "expiry": "2026-07-18T11:00:00.000Z",
      "parameters": "Curve25519/32byte random key",
      "keyValue": "base64-hip-public-key..."
    },
    "nonce": "base64-32-random-bytes..."
  }
}

POST /api/v3/hip/health-information/request — the push instruction, HIP side

Inbound ABDM callback — ABDM instructs this facility, acting as HIP, to actually send the encrypted FHIR data to the requesting HIU. This is the callback that drives the actual outbound transfer — the HIP calls notify (.../data-flow/v3/health-information/notify) once the push completes, which ABDM forwards to the HIU.

{
  "transactionId": "abdm-transfer-id-...",
  "hiRequest": {
    "consent": { "id": "9c1e6a70-6e2f-4b9b-9f0a-1a2b3c4d5e6f" },
    "dateRange": { "from": "2025-01-01T00:00:00.000Z", "to": "2026-01-01T00:00:00.000Z" },
    "dataPushUrl": "https://requesting-hiu.example.org/on-request",
    "keyMaterial": {
      "cryptoAlg": "ECDH",
      "curve": "Curve25519",
      "dhPublicKey": { "keyValue": "..." },
      "nonce": "..."
    }
  }
}

Purge on REVOKED / EXPIRED — mandatory data deletion

Every status transition (from any callback above) runs through the shared updateConsentStatus. When the new status is REVOKED or EXPIRED, three things are purged, per ABDM's compliance rule that HIUs and HIPs "mandatorily get rid of the health records... for whose consents have been revoked [or] expired":

  1. Consent.artefactJson — for consents held as HIP (role: 'HIP'), the permission/careContexts detail is stripped down to just { role, abdmArtefactId, purgedAt, purgedReason }, so a late-arriving callback can still find the row, but it no longer authorizes a further data push.
  2. FhirBundleExchange rows with direction: INBOUND for that consentId — the copies held as HIU, pulled in from a HIP under this consent, are hard-deleted. (Outbound rows — data this system pushed out as HIP — are untouched; a HIU-side consent's lifecycle doesn't affect the source EMR's own records.)
  3. FhirResource rows (the LHR-normalized records — see next section) for that consentId — deleted too, so a revoked/expired consent doesn't leave records fully visible through the LHR views, which don't otherwise check live consent status on every read.

Health Documents — HIP Operator Document Upload

In plain terms: this is how a facility's staff manually upload a patient document (a PDF discharge summary, a scanned lab report, etc.) into the system so it becomes available to hand over the next time an HIU requests that patient's records under a valid consent — for facilities that don't have a live HIS/EMR integration producing FHIR data automatically.

All routes require X-HIP-ID (requireXHipId applied at the router level — mounted at /v1/health-documents).

POST /v1/health-documents

Uploads a new document.

{
  "abhaAddress": "ravi.kumar@sbx",
  "careContextReference": "ENC-2026-00123",
  "hiType": "DISCHARGESUMMARY",
  "title": "Discharge Summary - Cardiology",
  "content": "JVBERi0xLjQKJcOkw7zDtsO...",
  "mimeType": "application/pdf",
  "fileSize": 245678
}

GET /v1/health-documents

Lists documents for the resolved facility, optionally filtered by abhaAddress and/or hiType. Query: limit (1–100, default 20), offset.

GET /v1/health-documents/:id

Fetches one document, including its full content (base64) and fhirBundle (omitted from the list view for size).

DELETE /v1/health-documents/:id

Deletes a single document. 404 if not found.

DELETE /v1/health-documents

Bulk delete, scoped by the resolved facility and an optional abhaAddress query filter. Refuses to run (throws) if neither a resolvable facility nor an abhaAddress filter is present — guarding against accidentally wiping the entire table.


LHR — Local Health Record (normalized, queryable records)

In plain terms: once encrypted FHIR bundles are decrypted and received (in the health-information-transfer step above), they're still just an opaque blob per care context. LHR is the layer that unpacks those bundles into individual, queryable FHIR resources (one row per Observation, Condition, DocumentReference, etc.) so the PHR app (and any dashboard) can show a proper timeline, group records by source hospital, and open a single record without re-parsing a whole bundle every time.

All routes require a PHR Bearer token: requireAuth (extracts the token) + requirePhrAbhaAddress (decodes the sub claim as a candidate ABHA address, then confirms the token is genuinely ABDM-issued by calling ABDM's own GET /profile with it — a short-lived cache avoids round-tripping on every request). Mounted at /v1/phr/lhr and /v2/phr/lhr (identical router).

GET /v1/phr/lhr/timeline

The main paginated record feed.

{
  "success": true,
  "data": {
    "data": [
      {
        "id": "clx1a2b3c0000qzrm8h6j9f2a",
        "resourceType": "DiagnosticReport",
        "resourceDate": "2026-06-01T00:00:00.000Z",
        "hipId": "IN0310000741_1",
        "careContextReference": "ENC-2026-00123",
        "hiType": "DIAGNOSTICREPORT",
        "data": { "resourceType": "DiagnosticReport", "...": "..." },
        "createdAt": "2026-07-18T09:00:00.000Z"
      }
    ],
    "nextCursor": "clx1a2b3c0001...",
    "total": 42
  }
}

GET /v1/phr/lhr/grouped

Same underlying FhirResource rows (capped at 500), grouped by hipId, each group annotated with hipName and lastSyncedAt from the DataSource table.

GET /v1/phr/lhr/sources

Lists the distinct HIPs this patient has data from, with a per-resource-type count (LongitudinalIndex).

GET /v1/phr/lhr/records/:id

Fetches one normalized resource. Ownership is enforced by matching patientAbhaAddress against the token-verified address — 404 if the id exists but belongs to someone else.

GET /v1/phr/lhr/records/:id/bundle

Returns the full decrypted FHIR document bundle that record belongs to (not just the one resource) — resolved via the record's bundleExchangeId back into the stored FhirBundleExchange row, so the UI can render the whole Composition and its section references. Falls back to wrapping the lone resource as a collection bundle for older records that predate bundle linkage.

POST /v1/phr/lhr/sync

Manually queues a background sync job for a patient (re-triggers the data-flow for their granted consents).

GET /v1/phr/lhr/self-fetch-status

A diagnostic endpoint (not a general product feature) that walks a patient's recent self-fetch (PATRQT) consents and reports exactly where each one stalled in the pipeline — REQUESTED-never-granted, GRANTED-but-no-artefact, artefact-but-no-data-request, requested-but-HIP-never-pushed, or pushed-but-decrypt-failed — plus raw recent inbound callback hits for support/debugging. Query: optional hipId filter.

POST /v1/phr/lhr/test-loopback

A test-only endpoint (auth required, but no requirePhrAbhaAddress) that locally drives the HIP→HIU data transfer for a granted self-fetch consent using seeded data, to validate the decrypt/store tail without depending on ABDM's real data-flow routing. Not part of the production consent flow.


HIU Subscription — Health Locker / Standing Subscription Management

In plain terms: a normal consent request is one-off — the HIU asks, the patient grants, data flows once (or on a schedule defined at grant time). A subscription is different: it's a standing arrangement where the patient pre-authorizes an HIU to be notified whenever new records show up at a given HIP, without a fresh consent dialog every time. This module manages that standing relationship (and the related "health locker" registration), from the facility's side.

Mounted at /v1/hiu-subscription (composed of /requests, /lockers, and root /:subscriptionId), all operator routes require X-HIP-ID.

POST /v1/hiu-subscription/requests — initiate a subscription

{
  "subscription": {
    "purpose": { "text": "Care Management", "code": "CAREMGT" },
    "patient": { "id": "ravi.kumar@sbx" },
    "hiu": { "id": "IN0310000702_1" },
    "hips": [{ "id": "IN0310000741_1", "name": "City Hospital" }],
    "categories": ["LINK", "DATA"],
    "period": { "from": "2026-07-18T00:00:00.000Z", "to": "2027-07-18T00:00:00.000Z" }
  }
}

Note: hips is required by this schema even though ABDM's own docs mark it optional — empirically, the sandbox silently 202s-then-drops a subscription request with an empty/omitted hips list (no on-init ever arrives), so this codebase enforces at least one entry.

GET /v1/hiu-subscription/requests, GET /v1/hiu-subscription/requests/combined

Lists this facility's subscription requests (and, for /combined, the resulting Subscription rows alongside them).

POST /v1/hiu-subscription/requests/:requestId/approve / /deny

Local-only operator actions on a pending request row (same "doesn't call ABDM" caveat as consent approve/deny — subscriptions are also patient-granted via ABDM, not HIP-approved).

GET /v1/hiu-subscription/requests/:requestId/subscription

Fetches the resulting Subscription row for a given request, once granted.

GET /v1/hiu-subscription/:subscriptionId, PUT /v1/hiu-subscription/:subscriptionId, POST /v1/hiu-subscription/:subscriptionId/enable, POST /v1/hiu-subscription/:subscriptionId/disable

CRUD/toggle on an established Subscription row (details JSON, enabled flag).

GET /v1/hiu-subscription/lockers, POST /v1/hiu-subscription/lockers, GET /v1/hiu-subscription/lockers/:lockerId

Manages HealthLocker registration — required for this facility's entity to be eligible for the subscription mechanism at all (an entity must be registered as HEALTH_LOCKER with ABDM/NHA approval).

Subscription Callbacks (inbound)

POST /api/v3/hiu/hiecm/subscription-requests/on-init — Inbound ABDM callback. Acknowledges the subscription-init submission and assigns subscriptionRequest.id.

{ "subscriptionRequest": { "id": "b2f8b2b0-..." }, "response": { "requestId": "our-request-id" } }

Matches the pending request by the stored requestId, stamps abdmSubscriptionId, emits subscription:initialized.

POST /api/v3/hiu/subscription-requests/hiu/notify — Inbound ABDM callback. Patient's approve/deny decision.

{ "notification": { "subscriptionRequestId": "b2f8b2b0-...", "status": "GRANTED" } }

On GRANTED: creates/enables the Subscription row and fires a SUBSCRIPTION_APPROVED notification. On DENIED/REVOKED/EXPIRED: disables any existing active Subscription row and fires the matching notification. Acknowledges back to ABDM (onNotify) with { "acknowledgement": { "status": "OK", "subscriptionId": "..." } }.

POST /api/v3/hiu/subscription/notify — Inbound ABDM callback. The actual ongoing subscription events — new care contexts becoming available.

{
  "event": {
    "id": "e1a2b3c0-...",
    "published": "2026-07-18 09:03:26.994",
    "category": "DATA",
    "content": {
      "patient": { "id": "ravi.kumar@sbx" },
      "hip": { "id": "IN0310000741_1" },
      "contexts": [
        {
          "hiType": "DiagnosticReport",
          "careContexts": [
            { "patientReference": "ravi.kumar@sbx", "careContextReference": "ENC-2026-00456" }
          ]
        }
      ]
    }
  }
}

Note: published is a plain string, never parsed as a Date — ABDM sends it in a non-ISO, space-separated format ("2026-07-02 09:03:26.994") that would fail strict ISO-8601 validation.