From f90d1df284a06937510ad729cf2f1ff403262928 Mon Sep 17 00:00:00 2001 From: glebmark <87950237+glebmark@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:18:25 +0400 Subject: [PATCH] [misc] provider-member-match updated doc --- SUMMARY.md | 4 +- .../CMS-0057-F/developer-docs/README.md | 9 + .../provider-access-api/README.md | 14 ++ .../provider-member-match.md} | 166 +++++++++++++----- redirects.yaml | 2 + 5 files changed, 148 insertions(+), 47 deletions(-) create mode 100644 docs/solutions/CMS-0057-F/developer-docs/README.md create mode 100644 docs/solutions/CMS-0057-F/developer-docs/provider-access-api/README.md rename docs/{api/other/plan-api/provider-member-match/README.md => solutions/CMS-0057-F/developer-docs/provider-access-api/provider-member-match.md} (75%) diff --git a/SUMMARY.md b/SUMMARY.md index cf8fe9734..56d592bb6 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -204,7 +204,6 @@ * [PractitionerRole](api/other/plan-api/provider-directory-api/practitionerrole.md) * [Organization](api/other/plan-api/provider-directory-api/organization.md) * [OrganizationAffiliation](api/other/plan-api/provider-directory-api/organizationaffiliation.md) - * [$provider-member-match](api/other/plan-api/provider-member-match/README.md) * [Archive/Restore API](api/other/archive-restore-api/README.md) * [Create-archive](api/other/archive-restore-api/create-archive.md) * [Restore-archive](api/other/archive-restore-api/restore-archive.md) @@ -501,6 +500,9 @@ * [Coverage Requirements Discovery (CRD)](solutions/CMS-0057-F/coverage-requirements-discovery-crd/README.md) * [CDS Hooks API](solutions/CMS-0057-F/coverage-requirements-discovery-crd/cds-hooks-api.md) * [Prior Authorization Support (PAS) API](solutions/CMS-0057-F/prior-authorization-support-pas-api.md) + * [Developer Docs](solutions/CMS-0057-F/developer-docs/README.md) + * [Provider Access API](solutions/CMS-0057-F/developer-docs/provider-access-api/README.md) + * [$provider-member-match](solutions/CMS-0057-F/developer-docs/provider-access-api/provider-member-match.md) ## Reference diff --git a/docs/solutions/CMS-0057-F/developer-docs/README.md b/docs/solutions/CMS-0057-F/developer-docs/README.md new file mode 100644 index 000000000..b9f3a0823 --- /dev/null +++ b/docs/solutions/CMS-0057-F/developer-docs/README.md @@ -0,0 +1,9 @@ +--- +description: Developer documentation for the CMS-0057-F APIs delivered with Aidbox — Patient Access, Provider Directory, Provider Access, and Payer-to-Payer. +--- + +# Developer Docs + +Developer-facing reference for the CMS-0057-F API suite. Each API has its own page covering the base URL, the authentication flow, supported FHIR operations, search parameters, and request/response examples. + +* [Provider Access API](provider-access-api/README.md) — backend services authentication, member matching, bulk data export. diff --git a/docs/solutions/CMS-0057-F/developer-docs/provider-access-api/README.md b/docs/solutions/CMS-0057-F/developer-docs/provider-access-api/README.md new file mode 100644 index 000000000..fb57414b4 --- /dev/null +++ b/docs/solutions/CMS-0057-F/developer-docs/provider-access-api/README.md @@ -0,0 +1,14 @@ +--- +description: Da Vinci PDex Provider Access API — backend services auth, member matching, and bulk clinical data export for treating providers. +--- + +# Provider Access API + +The [Da Vinci PDex Provider Access API](https://build.fhir.org/ig/HL7/davinci-epdx/) lets a provider attest to a treatment relationship with a payer's members and pull their clinical data in bulk. The flow has two stages: + +1. **Match members.** Submit a batch of patient demographics + coverage + treatment-relationship attestation. The payer returns one or more `Group` resources identifying matched members, members the payer could not match, and members who have opted out of provider data sharing. +2. **Export data.** Use the `MatchedMembers` Group ID with `$davinci-data-export` to retrieve the patients' clinical data as ndjson via the FHIR Bulk Data async pattern. + +{% content-ref url="provider-member-match.md" %} +[provider-member-match.md](provider-member-match.md) +{% endcontent-ref %} diff --git a/docs/api/other/plan-api/provider-member-match/README.md b/docs/solutions/CMS-0057-F/developer-docs/provider-access-api/provider-member-match.md similarity index 75% rename from docs/api/other/plan-api/provider-member-match/README.md rename to docs/solutions/CMS-0057-F/developer-docs/provider-access-api/provider-member-match.md index b5b1890ac..ca97c207c 100644 --- a/docs/api/other/plan-api/provider-member-match/README.md +++ b/docs/solutions/CMS-0057-F/developer-docs/provider-access-api/provider-member-match.md @@ -4,50 +4,71 @@ description: Da Vinci PDex Provider Access $provider-member-match operation — # $provider-member-match -The `$provider-member-match` operation implements the [Da Vinci PDex Provider Access API](http://hl7.org/fhir/us/davinci-pdex/STU2.1/). It lets a provider submit a batch of members (patient demographics, coverage, and a treatment-relationship attestation) and receive one or more `Group` resources whose IDs can then be used with `$davinci-data-export` to pull clinical data in bulk. +The `$provider-member-match` operation implements the [Da Vinci PDex Provider Access API](https://build.fhir.org/ig/HL7/davinci-epdx/). It lets a provider submit a batch of members (patient demographics, coverage, and a treatment-relationship attestation) and receive one or more `Group` resources whose IDs can then be used with `$davinci-data-export` to pull clinical data in bulk. -The operation follows the [FHIR Bulk Data async pattern](https://hl7.org/fhir/uv/bulkdata/export.html#bulk-data-kick-off-request): kick-off returns `202 Accepted` with a `Content-Location` header, the client polls the status URL, and once processing is complete downloads an ndjson file containing the output `Parameters` resource. +The operation follows the [FHIR Bulk Data async pattern](https://hl7.org/fhir/uv/bulkdata/export.html#bulk-data-kick-off-request): the kick-off request `POST [base]/fhir/Group/$provider-member-match` returns `202 Accepted` with a `Content-Location` header, the client polls the status URL, and once processing is complete downloads an ndjson file containing the output `Parameters` resource. -{% hint style="info" %} -**Spec references** (pinned to PDex STU 2.1): - -* [OperationDefinition-ProviderMemberMatch](http://hl7.org/fhir/us/davinci-pdex/STU2.1/OperationDefinition-ProviderMemberMatch.html) -* [Input profile: provider-parameters-multi-member-match-bundle-in](http://hl7.org/fhir/us/davinci-pdex/STU2.1/StructureDefinition-provider-parameters-multi-member-match-bundle-in.html) -* [Output profile: provider-parameters-multi-member-match-bundle-out](http://hl7.org/fhir/us/davinci-pdex/STU2.1/StructureDefinition-provider-parameters-multi-member-match-bundle-out.html) +{% hint style="warning" %} +`$provider-member-match` is **not part of Aidbox core**. It is delivered by the [Aidbox PDex / Provider Access interop app](https://github.com/HealthSamurai/interop), which registers itself with Aidbox as a FHIR `App` resource and handles the operation via HTTP-RPC. You must run and register the interop app against your Aidbox instance before these endpoints are available. The PDex 2.1 FHIR package (`hl7.fhir.us.davinci-pdex#2.1.0`) also needs to be loaded in Aidbox so the input/output profiles are known to `$validate`. {% endhint %} {% hint style="warning" %} `$provider-member-match` is **always asynchronous**. Clients must include the `Prefer: respond-async` header on the kick-off request. Requests without this header are rejected with `400 Bad Request`. {% endhint %} -## How it works +{% hint style="info" %} +**Spec references** (PDex 2.1): + +* [OperationDefinition: PDex Provider-Member-Match Operation](https://build.fhir.org/ig/HL7/davinci-epdx/OperationDefinition-ProviderMemberMatch.html) +* [Input profile: Provider $multi-member-match Request](https://build.fhir.org/ig/HL7/davinci-epdx/StructureDefinition-provider-parameters-multi-member-match-bundle-in.html) +* [Output profile: Provider $multi-member-match Response](https://build.fhir.org/ig/HL7/davinci-epdx/StructureDefinition-provider-parameters-multi-member-match-bundle-out.html) +* Output Group profiles: [pdex-treatment-relationship](https://build.fhir.org/ig/HL7/davinci-epdx/StructureDefinition-pdex-treatment-relationship.html), [pdex-member-no-match-group](https://build.fhir.org/ig/HL7/davinci-epdx/StructureDefinition-pdex-member-no-match-group.html), [pdex-member-opt-out](https://build.fhir.org/ig/HL7/davinci-epdx/StructureDefinition-pdex-member-opt-out.html) +{% endhint %} + +## Overview Each submitted member goes through a deterministic evaluation pipeline and lands in exactly one of three output buckets: | Bucket | Profile | Code | Meaning | | --- | --- | --- | --- | -| `MatchedMembers` | `pdex-treatment-relationship` | `match` | Demographics matched a payer member, treatment attestation is valid, and the member has not opted out. The Group ID of this bucket is what the provider feeds into `$davinci-data-export`. | -| `NonMatchedMembers` | `pdex-member-no-match-group` | `nomatch` | No match was found, the match was ambiguous, or the treatment attestation was invalid. | -| `ConsentConstrainedMembers` | `pdex-member-opt-out` | `consentconstraint` | Demographics matched, but the member has an active opt-out `Consent` on file. | +|`MatchedMembers` |`pdex-treatment-relationship` |`match` | Demographics matched a payer member, treatment attestation is valid, and the member has not opted out. The Group ID of this bucket is what the provider feeds into `$davinci-data-export`. | +|`NonMatchedMembers` |`pdex-member-no-match-group` |`nomatch` | No match was found, the match was ambiguous, or the treatment attestation was invalid. | +|`ConsentConstrainedMembers` |`pdex-member-opt-out` |`consentconstraint` | Demographics matched, but the member has an active opt-out `Consent` on file. | Per-member failures do not fail the whole batch — problematic members are routed to `NonMatchedMembers`. +### Per-member evaluation pipeline + +
+ ```mermaid graph TD A(Input member):::blue2 --> B(Validate attestation):::green2 - B -->|invalid| N(NonMatchedMembers):::neutral1 B -->|valid| C(Match demographics):::green2 - C -->|no match| N C -->|matched| D(Check opt-out):::green2 + B -->|invalid| N(NonMatchedMembers):::neutral1 + C -->|no match| N D -->|opted out| O(ConsentConstrainedMembers):::yellow2 D -->|not opted out| M(MatchedMembers):::violet2 + N ~~~ O ~~~ M ``` -## Endpoint +
-``` -POST [base]/fhir/Group/$provider-member-match -``` +### Per-member edge cases + +The pipeline never rejects an entire batch. Every input member resolves to exactly one bucket, even when the input is malformed: + +| Condition | Bucket | +| --- | --- | +| `Consent` part missing or `status` not `active` | `NonMatchedMembers` | +| Submitted Patient missing any of `family`, `given[0]`, `birthDate`, `gender` | `NonMatchedMembers` | +| Demographic search returns zero payer Patients | `NonMatchedMembers` | +| Demographic search returns more than one payer Patient (ambiguous) | `NonMatchedMembers` | +| `Coverage.subscriberId` is present but no payer Patient has a matching Coverage | `NonMatchedMembers` | +| Unhandled exception while evaluating a single member | `NonMatchedMembers` (with reason in `Task` logs) | +| Match succeeds and matched Patient has an active opt-out `Consent` | `ConsentConstrainedMembers` | +| Match succeeds and no opt-out exists | `MatchedMembers` | ## Workflow @@ -69,6 +90,53 @@ POST [base]/fhir/Group/$provider-member-match {% endstep %} {% endstepper %} +### Async sequence + +
+ +```mermaid +sequenceDiagram + participant C as Provider client + participant A as Aidbox + participant I as Interop app + C->>A: POST /fhir/Group/$provider-member-match
Prefer: respond-async + A->>I: HTTP-RPC kick-off + I->>A: PUT Task (status=requested) + I-->>A: 202 + Content-Location + A-->>C: 202 + Content-Location + Note over I: background future:
match → opt-out → group build + loop Poll + C->>A: GET .../$provider-member-match-status/{id} + A->>I: HTTP-RPC status + alt Task in-progress + I-->>A: 202 + Retry-After + A-->>C: 202 + Retry-After + else Task completed + I-->>A: 200 + Bulk Data manifest + A-->>C: 200 + manifest + end + end + C->>A: GET /output/{id}.ndjson + A->>I: HTTP-RPC ndjson + I-->>A: 200 + Parameters (ndjson) + A-->>C: 200 + Parameters (ndjson) +``` + +
+ +## Authorization + +`$provider-member-match` is a backend-services API: callers are provider organizations, not end users. The interop app authenticates the request via the OAuth client that Aidbox issues a token to (typically through the SMART [Backend Services / `client_credentials`](https://www.hl7.org/fhir/smart-app-launch/backend-services.html) flow). + +| Concern | Requirement | +| --- | --- | +| Auth flow | OAuth 2.0 `client_credentials` (SMART Backend Services). The bearer token is presented to Aidbox; Aidbox forwards the resolved `oauth/client` to the interop app over HTTP-RPC. | +| Provider identity | The OAuth `Client` resource representing the provider organization must carry an NPI in `Client.identifier[*]` with `system = http://hl7.org/fhir/sid/us-npi`. The interop app reads this entry and stamps it on `Task.requester` and on the `MatchedMembers` Group. If no NPI is found the provider is recorded as `"unknown"`. | +| Required Aidbox access | The interop app needs read access to `Patient`, `Coverage`, `Consent`, `Organization` (for payer NPI lookup) and read/write access to `Task`, `Group`, `Binary` in the same Aidbox instance. The app is registered as a FHIR `App` resource on startup; granting the `App` an access policy that allows these resource types is sufficient. | +| App registration | The interop app must be running and registered (its `App` resource present in Aidbox). Without it, `POST /fhir/Group/$provider-member-match` returns Aidbox's default "operation not found" response. | + +UDAP B2B token validation, mTLS, and dynamic client registration are **not** implemented today — see [Limitations](#limitations). + ## Input A FHIR `Parameters` resource with one or more `MemberBundle` parameters. Each `MemberBundle` contains: @@ -254,11 +322,11 @@ The operation tracks progress with a standard FHIR `Task` resource. Responses ma | Task status | HTTP | Headers | Body | | --- | --- | --- | --- | -| `requested` | 202 | `Retry-After: 5` | (empty) | -| `in-progress` | 202 | `Retry-After: 5`, `X-Progress: Processing members` | (empty) | +| `requested` | 202 | `Retry-After: 5` | — | +| `in-progress` | 202 | `Retry-After: 5`, `X-Progress: Processing members` | — | | `completed` | 200 | `Content-Type: application/json` | Bulk Data manifest | -| `failed` | 500 | | `OperationOutcome` with failure diagnostics | -| not found | 404 | | `OperationOutcome` | +| `failed` | 500 | — | `OperationOutcome` with failure diagnostics | +| not found | 404 | — | `OperationOutcome` | {% tabs %} {% tab title="In progress" %} @@ -545,15 +613,6 @@ A background job inside the interop app runs hourly and: Cleanup only touches PDex Groups — the scan filters on `_profile=`. Non-PDex Groups in the same Aidbox instance are left alone. -## Provider identity - -The provider's NPI is extracted from the authenticated OAuth client's `identifier` array (entry with system `http://hl7.org/fhir/sid/us-npi`). The NPI is used to: - -* Populate `Task.requester.identifier` on the kick-off Task. -* Populate the `identifier` array and the `characteristic[0].valueReference` on the `MatchedMembers` Group. - -If the OAuth client has no NPI identifier, the provider NPI falls back to `"unknown"` in the output Groups. - ## Error responses | Status | Condition | @@ -569,11 +628,11 @@ Individual member failures (validation issues, exceptions while evaluating one m ## End-to-end example -The snippets below reproduce the three-bucket demo used in internal testing. They assume an Aidbox instance with the PDex STU 2.1 package loaded and an OAuth client authorized to call the interop app. +The snippets below reproduce the three-bucket demo used in internal testing. They assume an Aidbox instance with the PDex STU 2.1 package loaded, the interop app registered, and an OAuth client authorized to call it. -{% stepper %} -{% step %} -**Seed the payer-side test data.** Post this `Bundle` once to create the Organization, payer-side Patients, Coverages, and an opt-out `Consent` for member-002. +### 1. Seed the payer-side test data + +Post this `Bundle` once to create the Organization, payer-side Patients, Coverages, and an opt-out `Consent` for `test-member-002`. ```http POST /fhir @@ -641,10 +700,10 @@ Content-Type: application/fhir+json ] } ``` -{% endstep %} -{% step %} -**Kick off.** Submit three members: one will match, one will hit the opt-out, one has no match. +### 2. Kick off + +Submit three members: one will match, one will hit the opt-out, one has no match. ```http POST /fhir/Group/$provider-member-match @@ -685,18 +744,18 @@ Prefer: respond-async ``` Response: `202 Accepted`, `Content-Location: [base]/fhir/Group/$provider-member-match-status/`. -{% endstep %} -{% step %} -**Poll.** Call the status URL until it returns `200 OK`. +### 3. Poll + +Call the status URL until it returns `200 OK`. ```http GET /fhir/Group/$provider-member-match-status/ ``` -{% endstep %} -{% step %} -**Download.** Use the `output[0].url` from the manifest to fetch the ndjson. +### 4. Download + +Use the `output[0].url` from the manifest to fetch the ndjson. ```http GET /output/.ndjson @@ -709,5 +768,20 @@ Expected result distribution: | Johnson, Robert | Yes (`test-member-001`) | No | `MatchedMembers` | | Williams, Sarah | Yes (`test-member-002`) | Yes | `ConsentConstrainedMembers` | | Unknown, Nobody | No | — | `NonMatchedMembers` | -{% endstep %} -{% endstepper %} + +## Limitations + +The current implementation is intentionally MVP-scoped. The following items are recognized gaps versus the PDex 2.1 spec / production expectations: + +| Area | Status | +| --- | --- | +| Matching algorithm | Deterministic exact match only (`family`, `given[0]`, `birthDate`, `gender`, optional `subscriberId`). No fuzzy / probabilistic matching, no confidence scoring, no multi-tier fall-through. | +| Opt-out scope enforcement | All opt-out scopes (`global`, `provider-specific`, `purpose-specific`, `payer-specific`, `provider-category`) are treated identically — any active `deny` constrains the match. Targeting the requesting provider's NPI for `provider-specific` opt-outs is not yet enforced. | +| Authentication hardening | UDAP B2B token validation, mTLS, and dynamic client registration are not implemented. The operation relies on whatever OAuth flow Aidbox is configured for. | +| Rate limiting | No throttling, no `429 Too Many Requests`, no per-client concurrency caps. | +| Treatment-relationship verification | Limited to `Consent.status = "active"` and presence of the resource. The attestation claim itself is not cross-checked against external sources. | +| Sandbox / interactive console | None — all examples are run against a real Aidbox instance. | + +## What's next + +After receiving a `MatchedMembers` Group, the provider uses its ID to call `$davinci-data-export` (FHIR Bulk Data group export) on the same Aidbox instance to retrieve the matched patients' clinical data as ndjson. Documentation for that operation will live alongside this page in the [Provider Access API](README.md) section. diff --git a/redirects.yaml b/redirects.yaml index 1a5591fe2..2ce8a13c4 100644 --- a/redirects.yaml +++ b/redirects.yaml @@ -3,6 +3,8 @@ redirects: contact-us: overview/contact-us.md access-control/authentication/access-to-aidboxui: access-control/authentication/README.md api/other/plan-api/plan-api-overview: api/other/plan-api/README.md + api/other/plan-api/provider-member-match: solutions/CMS-0057-F/developer-docs/provider-access-api/provider-member-match.md + api/other/plan-api/provider-member-match/README: solutions/CMS-0057-F/developer-docs/provider-access-api/provider-member-match.md developer-experience/aidbox-sdk/aidbox-javascript-sdk: deprecated/deprecated/other/aidbox-sdk/aidbox-javascript-sdk.md developer-experience/aidbox-sdk/nodejs-sdk: deprecated/deprecated/other/aidbox-sdk/nodejs-sdk.md developer-experience/aidbox-sdk/python-sdk: deprecated/deprecated/other/aidbox-sdk/python-sdk.md