Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions docs/solutions/CMS-0057-F/developer-docs/README.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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 %}
Original file line number Diff line number Diff line change
Expand Up @@ -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

<div align="center">

```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
</div>

```
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

Expand All @@ -69,6 +90,53 @@ POST [base]/fhir/Group/$provider-member-match
{% endstep %}
{% endstepper %}

### Async sequence

<div align="center">

```mermaid
sequenceDiagram
participant C as Provider client
participant A as Aidbox
participant I as Interop app
C->>A: POST /fhir/Group/$provider-member-match<br/>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:<br/>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)
```

</div>

## 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:
Expand Down Expand Up @@ -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" %}
Expand Down Expand Up @@ -545,15 +613,6 @@ A background job inside the interop app runs hourly and:

Cleanup only touches PDex Groups — the scan filters on `_profile=<pdex-treatment-relationship,pdex-member-no-match-group,pdex-member-opt-out>`. 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 |
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -685,18 +744,18 @@ Prefer: respond-async
```

Response: `202 Accepted`, `Content-Location: [base]/fhir/Group/$provider-member-match-status/<task-id>`.
{% 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/<task-id>
```
{% 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/<task-id>.ndjson
Expand All @@ -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.
2 changes: 2 additions & 0 deletions redirects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading