Integration Contract: Main Orvo ↔ orvo-abha
Purpose of this document
This is the concrete contract for engineers wiring the main Orvo system — the primary product database and API that own patients, encounters, facilities, and staff (as distinct from orvo-abha, which owns nothing except ABDM protocol state) — to orvo-abha. It answers three questions precisely: what identity model links the two systems, which orvo-abha endpoints to call and when, and what belongs on which side of the boundary. It assumes you've read Architecture Overview for the actor glossary (HIP/HIU/ABHA/consent/care-context) — this document does not re-explain those terms.
Everything below is grounded in what the code actually does today, not an aspirational design — including calling out that AbdmPatientIdentity's multi-system-discovery path is live and exercised on every request, but permanently a no-op today because nothing ever writes to it (see §1).
1. The identity-linking model
Facility identity — live and wired today
Facility.mainAppFacilityId (in orvo-abha's own database, prisma/schema.prisma) is the actual, currently-used bridge field between the two systems. Every Facility row in orvo-abha carries:
| Field | Meaning |
|---|---|
id |
orvo-abha's own internal facility UUID — used everywhere inside orvo-abha (Consent.facilityId, CareContext.facilityId, etc.) |
mainAppFacilityId |
The main Orvo system's own facility ID. Required at facility-creation time (POST /v1/facilities, see the HIP Identity & Linking API doc); rejected with 422 if missing. |
hipId |
The ABDM-registered HIP identifier for this facility (e.g. IN0310000702_1) — globally unique across ABDM, not just within Orvo. |
hfrFacilityId |
The govt Health Facility Registry ID — reference/audit only, often blank until HFR registration completes; never used as a lookup key. |
The rule: main Orvo is the source of truth for "what is a facility" (name, address, staff roster, billing) — it creates the facility first, then registers it into orvo-abha via POST /v1/facilities passing its own facility ID as mainAppFacilityId. From that point on, every orvo-abha call that needs facility context uses either the X-HIP-ID header (ABDM's id) or resolves through the active-facility fallback described in the Platform API doc — main Orvo should always know and send the hipId it got back from that registration call, not try to re-derive it.
Practical integration point: if main Orvo ever needs to go from "my own facility record" to "the matching orvo-abha state," look it up by mainAppFacilityId, not by name or address matching. There is currently no dedicated GET /v1/facilities?mainAppFacilityId=... filter — GET /v1/facilities returns the full list and current callers filter client-side; if this becomes a hot path, add a query filter rather than always paging the full list.
Patient identity — the plumbing exists and runs on every discovery call, but nothing has ever written to it
Correcting an earlier version of this doc: patient-master-index.service.ts (patientMasterIndexService) is not dead code — multi-system-discovery.service.ts's multiSystemDiscoveryAggregator.discoverPatient() calls its getSystemIdentities() for real, and that aggregator is itself called live from user-linking.callback.service.ts on every user-initiated-linking discovery request (wrapped in a try/catch that falls back to DB-only care contexts on any failure).
What's actually true today: getSystemIdentities() runs a genuine AbdmPatientIdentity read on every discovery call. But nothing in the codebase ever calls registerConnector() or registerSystemIdentity() — no HIS/LIMS/RIS connector is registered, and no internal-patient-ID mapping is ever written. So in practice, that read always finds nothing, the aggregator always returns zero extra care contexts, and discovery silently falls back to DB-only results on every single call. The mechanism is live; it's just permanently empty because the write side was never wired to anything.
This means the integration point below isn't hypothetical scaffolding — it's the literal missing wiring that would turn an already-executing, already-tested code path from a permanent no-op into something that actually aggregates records across HIS/LIMS/RIS systems.
What actually happens today instead, for ordinary care-context data (as opposed to this multi-system aggregation path): orvo-abha's CareContext table stores abhaAddress/abhaNumber plus an optional free-text patientReference — this patientReference is meant to be a patient identifier from whatever system produced the care context (an HIS/EMR), but nothing currently requires it to be main Orvo's canonical patient ID, and nothing reads it back into main Orvo.
Recommended contract going forward (the shape to build, using the schema that already exists for it):
- When main Orvo creates or looks up ABHA identity for one of its patients (enrollment via
/v1/hip/enrollment/*, or login via/v1/hip/login/*, or a care-context link), main Orvo should call a new orvo-abha endpoint — or, until one exists, write directly via an internal service call topatientMasterIndexService.registerSystemIdentity(abhaAddress, facilityId, 'HIS', mainOrvoPatientId)— immediately after the ABHA address is known, passing main Orvo's own patient ID asinternalSystemId. - From then on, every orvo-abha record that involves this patient (
CareContext,Consent,FhirResource) is reachable byabhaAddresson orvo-abha's side; main Orvo reaches the same patient by its own ID and resolves theabhaAddressthrough this mapping when it needs to call an orvo-abha API on the patient's behalf (e.g. triggering a consent request, or checking LHR sync status). - Do not use
AbhaProfile.userIdorUserFacility.userIdas this bridge — those model a login user (who's authenticated into the PHR-style flows in this backend), which is a different concept from a patient record in main Orvo's clinical database. A hospital patient frequently has no login of their own; conflating the two will break the multi-facility/multi-visit-without-login case that's actually the common one for OP walk-ins. AbdmPatientIdentitynow has a DB-level@@unique([facilityId, abhaAddress])constraint (added to satisfy the NHA-mandated "one ABHA number tagged to one unique internal patient ID" requirement) — exactly one master identity row per (facility, ABHA address). A concurrent double-write for the same pair will surface as a PrismaP2002error; retry as a read (the row already exists) rather than treating it as a failure.
Until this mapping is actually wired up end-to-end, treat abhaAddress as the only reliable shared key between the two systems for patient-level data — pass it explicitly in every integration call rather than relying on an internal ID lookup that doesn't exist yet.
Staff / user identity — deliberately unresolved, don't build around it yet
JWT verification for orvo-hub facility-staff sessions is currently disabled in orvo-abha (requireAuth checks a bearer token is present, not that it's valid) because the Orvo staff/role model (doctor vs. admin vs. operator) hasn't been finalized. This is a known, deliberate state — see the orvo-abha-staff-auth-not-finalized note referenced from architecture.md. Do not design the main-Orvo integration around orvo-abha's current staff-auth behavior — assume it will change, and keep any staff-identity assumptions in the integration layer isolated so they're easy to update later rather than load-bearing throughout.
2. What each system owns (the actual boundary)
| Concern | Owned by | Notes |
|---|---|---|
| Patient demographics, appointments, billing, EMR/encounter data | Main Orvo | orvo-abha never stores a patient's clinical record independent of what it needs for ABDM exchange (HealthDocument, FhirResource — both are copies staged for ABDM, not the source of truth) |
| Facility roster, staff accounts, roles | Main Orvo | orvo-abha's Facility/UserFacility rows are minimal — just enough to scope ABDM operations, not a facility management system |
| ABHA identity (number/address, KYC status) | orvo-abha | Created/managed exclusively through ABDM; main Orvo should treat AbhaProfile in orvo-abha as the reference, and store only the resulting abhaAddress/abhaNumber string(s) itself if it needs to display them |
| Care-context linking state (which encounter is linked to ABDM) | orvo-abha | CareContext.linked is the source of truth for "does ABDM know about this record" |
| Consent lifecycle (requested/granted/denied/revoked/expired) | orvo-abha | Entirely an ABDM/orvo-abha concern; main Orvo should only ever observe this via API/webhook, never write to it directly |
| The actual FHIR medical-record bundles moving between HIPs/HIUs | orvo-abha | FhirBundleExchange/FhirResource — staged/normalized copies for ABDM exchange and PHR display; not meant to replace main Orvo's own clinical record store |
| Insurance claims (NHCX) | orvo-abha | NhcxClaim/NhcxPayment — main Orvo triggers submission (see below) but doesn't need its own copy of claim state unless it wants to display it |
Rule of thumb: if a question is "did ABDM say yes," the answer lives in orvo-abha. If a question is "what actually happened at this hospital," the answer lives in main Orvo. Neither system should try to become the source of truth for the other's concern — main Orvo should not cache/duplicate consent status beyond what it needs for a UI, and orvo-abha should not grow patient-demographic fields beyond what ABDM's own protocol requires (name/DOB/gender/mobile, needed for enrollment/discovery — see AbhaProfile).
3. Which orvo-abha endpoints main Orvo should call, and when
This section is the sequence a main-Orvo backend (not a frontend — a server-to-server integration) should follow for the two events that actually need cross-system coordination: onboarding a facility, and the patient-visit lifecycle.
3.1 Facility onboarding (one-time, per facility)
Main Orvo creates the facility in its own database first (name, address, staff, billing setup — whatever main Orvo's own model requires).
Main Orvo calls
POST /v1/facilitieson orvo-abha with{ name, mainAppFacilityId: <main Orvo's facility id>, hipId, hipName }. If the facility doesn't yet have an ABDM HIP ID (new to ABDM entirely), that registration happens through NHA's own bridge-onboarding process outside this API —hipIdhere assumes ABDM's facility-registration/HFR step has already produced one;PUT /v1/gateway/bridge-services(see the HIP Identity & Linking doc) is how an already-approved bridge adds a new service under it.curl -X POST https://abha.orvo.app/v1/facilities \ -H 'Content-Type: application/json' \ -d '{ "name": "City Hospital", "mainAppFacilityId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "hipId": "IN0310000741_1", "hipName": "City Hospital" }'Store the returned orvo-abha
Facility.idandhipIdback on main Orvo's own facility record —hipIdis what belongs in theX-HIP-IDheader on every subsequent facility-scoped orvo-abha call, notmainAppFacilityIdor orvo-abha's internalid.curl https://abha.orvo.app/v1/settings/facility \ -H 'X-HIP-ID: IN0310000741_1'Optionally configure
PATCH /v1/settings/facility(data classification, consent defaults, auto-approve rules) andPATCH /v1/settings/credentials(ABDM client id/secret for this facility's bridge/environment) — see the Platform API doc.
3.2 Patient visit lifecycle (per encounter)
This is the sequence that actually needs the two systems to stay in step. It assumes main Orvo is the one driving the patient-facing UI (registration desk, EMR) and calls orvo-abha as a backend service, or that the existing orvo-hub/orvo-web frontends are doing so on main Orvo's behalf — the sequence is the same either way.
Patient has no ABHA yet → main Orvo (via its UI or a server call) drives
POST /v1/hip/enrollment/request→/verify→ (/mobile/request+/mobile/verifyif needed) →/address/suggestions/:txnId→POST /v1/hip/enrollment/address. Result: anabhaAddress. Store this on the main-Orvo patient record immediately — it's the shared key described in §1. Patient already has an ABHA →POST /v1/hip/login/request/otp→/verify/otp→ (/verify/userif abha-number hint with multiple addresses) → same result, anabhaAddress.Main Orvo creates/updates its own encounter record as normal (this never touches orvo-abha).
To make that encounter visible to ABDM as a care context: either (a) call
POST /v1/health-documentsif main Orvo wants orvo-abha to build the FHIR bundle from an uploaded document, or (b) rely on whatever HIS/converter integration already produces aCareContextrow (seefhir-converter.his.tsetc. in the Consent & Health Data doc) — either path upserts aCareContextrow keyed byabhaAddress+careContextReference.If this is a HIP-initiated link (facility already knows the ABHA, wants to push the link proactively):
POST /v1/link/token/generate→ wait forlink-token:received(Pusher, see §4) →POST /v1/hip-linking/patient/links/care-context→ wait for theon_carecontextresult reflected inCareContext.linked. If this is patient-initiated (patient does it themselves in the PHR app): nothing for main Orvo to drive — this happens entirely between orvo-phr and orvo-abha; main Orvo only needs to know the outcome (see §4).Requesting another facility's records for this patient (a genuinely new cross-facility scenario, e.g. main Orvo wants "pull this patient's history from their previous hospital before today's consult"):
POST /v1/consents/createwith the patient'sabhaAddressand the targethipIdif known (or omithipIdfor a blind self-fetch-style discovery — see the caveat in the Consent & Health Data doc about not pinning an unknown HIP). Then poll or subscribe for the grant (see §4), then read the resulting records viaGET /v1/phr/lhr/timeline(if reading as the patient's own PHR context) or the equivalent HIU-side query endpoints (GET /v1/consents/health-info/*).curl -X POST https://abha.orvo.app/v1/consents/create \ -H 'Content-Type: application/json' \ -H 'X-HIP-ID: IN0310000741_1' \ -d '{ "abhaAddress": "ravi.kumar@sbx", "purpose": "Care Management", "dataTypes": ["DiagnosticReport", "OPConsultation"], "hiuId": "IN0310000702_1" }' # -> 201, { data: { requestId, consentId, status: "REQUESTED" } } # The grant itself only ever happens on the patient's own ABHA app — nothing # in this call, or any call in this API, makes the hospital the approver.Insurance claims, if applicable to the visit:
POST /v1/nhcx/coverage-eligibility/checkbefore treatment,POST /v1/nhcx/preauth/submitfor scheduled procedures,POST /v1/nhcx/claim/submitafter discharge. Main Orvo should trigger these at the natural points in its own visit workflow (admission, procedure scheduling, discharge/billing) — orvo-abha has no visit-workflow concept of its own to hook into.
4. How main Orvo should observe async results — realtime events, not polling
Nearly everything in §3.2 is asynchronous (ABDM's protocol is callback-driven end to end — see Architecture Overview). Main Orvo has two ways to learn the outcome of a call it made:
- Pusher realtime events (preferred) — call
GET /v1/realtime/facility-channel(withX-Facility-ID) orGET /v1/realtime/channels(with a patient bearer token, for PHR-context flows) once, get back a channel name + Pusher public key + the full event-name catalog, then subscribe directly to Pusher. This is exactly how orvo-hub and orvo-phr already work — main Orvo's backend (or a service worker behind it) should do the same rather than inventing a separate mechanism. The full event catalog (consent lifecycle, care-context linking, health-info push, NHCX claim updates, notifications) is documented in the Platform API doc's Realtime section. - Polling fallback / reconnect gap-filling —
GET /v1/realtime/events?channel=...&since=...replays durable event copies for a channel, capped at ~1 hour of retention. Use this to fill gaps after a dropped connection, not as the primary integration mechanism — it is explicitly a fallback, and events older than roughly an hour are pruned.
Do not build a separate webhook receiver on main Orvo's side expecting orvo-abha to call it directly. orvo-abha does not originate outbound webhooks to arbitrary consumers today — its own inbound surface (from ABDM) and its outbound surface (to consumers) are Pusher events, plus the direct HTTP responses/query endpoints documented per-module. If main Orvo genuinely needs a durable server-to-server webhook (e.g. for an offline batch job that can't hold a Pusher connection open), that would be new work on orvo-abha's side, not something to route around by inventing a parallel channel.
5. What NOT to do
- Don't duplicate ABHA/KYC data into main Orvo's own database beyond
abhaAddress/abhaNumber. If a display name, DOB, or address is needed in a main-Orvo UI, fetch it live viaGET /v1/hip/login/profile(or the enrollment equivalent) rather than mirroringAbhaProfile— ABDM's own KYC data can change (e.g. address updates) and a stale mirrored copy is worse than a live call. - Don't treat orvo-abha's
Consent/CareContextrows as directly writable from main Orvo. Every write path into those tables in orvo-abha assumes it's either responding to an ABDM callback or driving an ABDM-facing API call — a direct DB write from outside would desync local state from what ABDM's gateway actually believes, which is exactly the class of bug the "stray trailing space in hipId" and "blind self-fetch must not pin an unknown HIP" issues documented elsewhere in these docs came from. - Don't assume synchronous consent approval. Even the "operator approve" endpoints (
POST /v1/consents/:consentId/approve) documented in the Consent & Health Data doc are a local-only convenience that does not call ABDM — real consent is only ever granted by the patient on their own Consent Manager app. Any main-Orvo UI that implies "the hospital approved this consent" needs to be worded and designed around that reality, not around a false promise of HIP-side approval. - Don't reuse the same ABDM bridge/client credentials across environments carelessly.
BridgeConfigis keyed by(bridgeId, environment)— sandbox and production are fully separate credential sets, and mixing them (e.g. testing against sandboxhipIds while pointed at production credentials) is a common source of silent ABDM-side failures (202-then-drop, no callback ever arriving) that looks identical to a real integration bug.
6. Error-handling contract: what to expect when a call fails
Because almost every call in §3 is a call into ABDM's territory rather than a normal internal API, "the call failed" means something more specific here than a typical 4xx/5xx, and main Orvo's error handling should be built around these distinctions rather than treating every non-2xx the same way:
- A
202 Acceptedis not success — it's "ABDM took the request." The only calls that return a final result synchronously are the read-only ones (GET /v1/consents/details/:requestId,GET /v1/phr/lhr/timeline, etc.) and the small set of purely-local writes (operator approve/deny, settings updates). Everything that talks to ABDM (consent/create,link/token/generate,hip-linking/*,nhcx/*) hands back202/201for "accepted for processing," with the real outcome arriving later via the realtime events in §4. Do not treat a202as "the consent was granted" or "the link succeeded" — those are separate, later, asynchronous facts. - A
4xxfrom an orvo-abha endpoint is a request-shape problem on main Orvo's side (missing required field, invalidhipId, a validation schema rejection) — retrying the identical request will fail identically. These map 1:1 to normal API error handling: fix the request, don't retry it as-is. - A
5xx, or a request that never resolves, is either an orvo-abha-side fault or (more often, for ABDM-facing calls) the gateway call itself failing before this service could even normalize a response. orvo-abha's own outbound gateway client already retries transient5xx/429once or twice with backoff internally (see the Operations Guide's Runtime Internals section) — by the time main Orvo sees a5xxfrom this API, that internal retry has already been exhausted, so a client-side retry is reasonable here, unlike the4xxcase above. - ABDM's own error phrases are normalized, and some carry a
retryableflag —abdm-errors.tsmaps known ABDM error strings/codes to an HTTP status, a user-facing message, and whether the underlying condition is worth retrying. See the ABDM Error Matrix for the phrase-by-phrase mapping this normalizer implements. Main Orvo should surface the normalized message to its own UI rather than a raw ABDM error string, and should only auto-retry a call whose response carriesretryable: true. - The "202-then-nothing" failure mode has no HTTP-level signal at all. ABDM accepted the request, and then silently never sent the callback that would have carried the real result — this is not represented as an error anywhere in the synchronous response, because from orvo-abha's point of view nothing failed yet, it's just still pending. Main Orvo needs its own timeout on any flow that depends on this (a consent grant, a care-context link) — if the realtime event or a status poll hasn't resolved within a reasonable window (minutes, not seconds — a human has to act on their ABHA app for a consent grant), treat it as failed and let the user retry, rather than waiting indefinitely. The Operations Guide's Troubleshooting section lists the common causes of this specific failure mode.
- Every outbound call orvo-abha makes to ABDM is logged to
GatewayTransaction, and every inbound callback toAbdmCallbackEvent. If main Orvo needs to diagnose why a specific call is stuck rather than just detecting that it is, those two tables (queryable today only via direct DB access through the bastion tunnel — see Operations Guide) are the ground truth, not application-level logs.