Skip to content
Open
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
1 change: 0 additions & 1 deletion SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
* [Run Payerbox](run-payerbox/README.md)
* [Architecture](run-payerbox/architecture.md)
* [Deploy](run-payerbox/deploy.md)
* [MPF Pipeline](run-payerbox/provider-directory-pipeline.md)
* [Maintain](run-payerbox/maintain/README.md)
* [Observability](run-payerbox/maintain/observability.md)
* [Upgrade](run-payerbox/maintain/upgrade.md)
Expand Down
64 changes: 58 additions & 6 deletions docs/api-reference/operations/mpf-pipeline-api.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
---
description: >-
MPF pipeline endpoint reference: the sync and refresh triggers and the
public directory endpoint.
MPF pipeline endpoint reference: the sync and refresh triggers, the export
scope settings, and the public directory endpoint.
---

# MPF Endpoints

The endpoints of the MPF provider-directory module. They mount only when the portal runs with `MPF_ENABLED=true`; otherwise every path below answers `404`. What the module publishes: [Provider Directory](../../interop-apis/provider-directory.md#mpf-feed-for-medicare-plan-finder). Setup: [Deploy](../../run-payerbox/deploy.md#mpf-provider-directory-pipeline).

| Endpoint | Purpose |
|---|---|
| `POST /admin/mpf/sync` | Full run: export, filter, bundle, publish |
| `POST /admin/mpf/refresh` | Re-bundle a previous export without re-exporting |
| `GET` / `PUT /admin/mpf/settings` | Read and write the export scope |
| `GET` / `HEAD /mpf-provider-directory/{contract}/{year}/{file}` | Public, the URL CMS crawls |

## Auth

The trigger endpoints take a Bearer token of a `client_credentials` client listed in `MPF_TRIGGER_CLIENT_IDS`. Mint the token from Aidbox:
Expand Down Expand Up @@ -99,15 +108,58 @@ Content-Type: application/json
{% endtab %}
{% endtabs %}

## GET /mpf-provider-directory/{contract}/{year}/{file}
## Trigger errors

Both triggers share these.

| Status | Meaning |
|---|---|
| `400` | `folder` missing on `/refresh`, a `contract` that is not `H` plus digits, or a `year` outside 2024–2099. |
| `403` | The token's client is not in `MPF_TRIGGER_CLIENT_IDS`. |
| `404` | `/refresh` only: the folder holds no files for the exported resource types. |
| `409` | A run is already in flight. Runs never overlap; retry after the current one finishes. |
| `500` | `MPF_EXPORT_CLIENT_ID` / `MPF_EXPORT_CLIENT_SECRET` unset, or the export scope could not be read. A failing scope read aborts rather than publishing the wrong scope. |

## GET / PUT /admin/mpf/settings

The export scope: which `InsurancePlan` ids and which network `Organization` ids the pipeline keeps. Stored on the admin Aidbox as a `DocumentReference`, so it survives restarts and upgrades, and both triggers resolve it at the start of every run. The Admin Portal edits the same values under **Settings → MPF**.

Same auth as the triggers.

{% tabs %}
{% tab title="GET response" %}
```json
{
"planIds": ["snp-plan", "map-plan"],
"networkIds": ["network-a", "network-b"],
"source": "settings"
}
```
{% endtab %}
{% tab title="PUT request" %}
```json
{
"planIds": ["snp-plan", "map-plan"],
"networkIds": ["network-a", "network-b"]
}
```
{% endtab %}
{% endtabs %}

`source` reports where the current values come from: `settings` once saved, `defaults` while the deployment still runs on the ids compiled into the image. `PUT` takes non-empty arrays of FHIR ids for both fields, answers `400` otherwise, and writes an `AuditEvent` on success.

## GET / HEAD /mpf-provider-directory/{contract}/{year}/{file}

Public, no auth: the endpoint CMS crawls. Proxies the storage bucket (which can stay private) and supports conditional GET, so repeat crawls only download files that changed. `index.json` lists the bundle URLs. The resources inside the bundles conform to the Plan-Net profiles.
Public, no auth: the endpoint CMS crawls. Proxies the storage bucket (which can stay private) and terminates conditional requests, so repeat crawls only download files that changed. `HEAD` returns the same headers without a body. Responses carry `Cache-Control: public, max-age=0, must-revalidate` plus the store's `ETag` and `Last-Modified`. `index.json` lists the bundle URLs.

```http
GET /mpf-provider-directory/H1234/2026/index.json
GET /mpf-provider-directory/H1234/2026/PractitionerRole-001.json
GET /mpf-provider-directory/H1234/2026/index.json
HEAD /mpf-provider-directory/H1234/2026/index.json
GET /mpf-provider-directory/H1234/2026/PractitionerRole-001.json
```

Path segments are validated before anything is fetched, and anything else answers `404`: `contract` is `H` followed by digits, `year` is a four-digit year in the 2000s, and `file` is either `index.json` or `<ResourceType>-<three digits>.json`.

| Status | Meaning |
|---|---|
| `200` | File served. |
Expand Down
34 changes: 34 additions & 0 deletions docs/interop-apis/provider-directory.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The [Da Vinci PDex Plan-Net IG](https://hl7.org/fhir/us/davinci-pdex-plan-net/)
- Public unauthenticated `GET` on the Plan-Net directory resource types (`Practitioner`, `PractitionerRole`, `Organization`, `Location`, `HealthcareService`); everything else stays authenticated.
- `Location.near` geographic search.
- FHIR Bulk Data `$export` for periodic directory snapshots.
- [MPF feed](#mpf-feed-for-medicare-plan-finder) for the CMS Medicare Plan Finder crawler, published daily as static bundles per Medicare Advantage contract and year (optional module).

## Caller and auth

Expand Down Expand Up @@ -120,3 +121,36 @@ GET <base>/fhir/PractitionerRole
## Bulk download

CMS recommends — but does not mandate — periodic full-directory downloads alongside REST search. Payerbox supports FHIR Bulk Data system-level `$export` with `_type=Practitioner,PractitionerRole,Organization,Location,HealthcareService` returning NDJSON, suitable for nightly snapshots a CDN or third party can mirror.

## MPF feed for Medicare Plan Finder

The CMS Medicare Plan Finder does not call the REST API above. It crawls a static feed: a manifest at a fixed URL, plus the FHIR `Bundle` files that manifest points to. Payerbox builds that feed from the same directory data and republishes it on a daily schedule, as an optional module of the [FHIR App Portal](../fhir-app-portal/README.md). Operators set it up in [Deploy](../run-payerbox/deploy.md#mpf-provider-directory-pipeline).

### What gets published

| Artifact | Shape |
|---|---|
| Manifest | `index.json`, a single object: `{"provider_urls": ["<absolute url>", …]}`. A file the manifest does not list is invisible to CMS. |
| Bundle files | `<ResourceType>-001.json`, `-002.json`, and so on. Each is a `Bundle` with `type: collection`, and every `entry` carries a `fullUrl` plus the resource. |
| Path | `<public base>/{contract}/{year}/{file}`. Files roll over at 1000 entries or 250 MiB, whichever comes first. |

Six resource types reach the feed, and the manifest lists them in dependency order: `InsurancePlan`, `Organization`, `Practitioner`, `PractitionerRole`, `Location`, `OrganizationAffiliation`. `HealthcareService` and `Endpoint` stay REST-only, so a consumer that needs services or electronic endpoints has to query the API rather than read the feed.

Each `Bundle` carries the run's generation timestamp in `meta.lastUpdated`, while every resource inside keeps the `meta.lastUpdated` it has in the FHIR engine. CMS tracks change per resource, so preserving the resource-level value is what makes an unchanged provider read as unchanged.

### Scope

The feed is not the whole directory. It carries the plans and networks the deployment declares in scope, and everything the graph pulls in with them: the affiliations and practitioner roles attached to those networks, the facility organizations those affiliations point at, and the practitioners and locations those roles reach. Everything else stays out. The in-scope plan and network ids are set per deployment in the Admin Portal, not baked into the image.

### Cadence and the year segment

CMS registers one URL per contract and calendar year, and crawls it daily with conditional requests, so an unchanged directory costs one round trip per file. Publish one run per day per contract. Around open enrollment, when both the current and next plan year are live, run a second sync with the next year to publish both concurrently.

### Choosing a path

| Path | When |
|---|---|
| Prebuilt pipeline | The published feed matches the six resource types and the scope model above. Configuration is buckets, credentials, and the in-scope ids. |
| Custom export flow | Different resource types, a different scope model, or post-processing between the export and the publish. Reuses the same `$export`, client, policy, and storage setup; a runnable example lives in the [Aidbox examples repository](https://github.com/Aidbox/examples). |

Endpoint contracts, status codes, and the settings API: [MPF Endpoints](../api-reference/operations/mpf-pipeline-api.md).
2 changes: 1 addition & 1 deletion docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ A new `payerbox` umbrella Helm chart deploys the full stack (portals, Interop AP

**Provider Directory**

- The CMS Medicare Plan Finder (MPF) provider-directory pipeline now runs its scope filters inside the `$export` query and gzip-compresses the export output. A runnable reference implementation is public in the [Aidbox examples](https://github.com/Aidbox/examples/tree/main/aidbox-features/medicare-plan-finder). See the [MPF Pipeline](run-payerbox/provider-directory-pipeline.md).
- The CMS Medicare Plan Finder (MPF) provider-directory pipeline now runs its scope filters inside the `$export` query and gzip-compresses the export output. A runnable reference implementation is public in the [Aidbox examples](https://github.com/Aidbox/examples/tree/main/aidbox-features/medicare-plan-finder). See [Deploy](run-payerbox/deploy.md#mpf-provider-directory-pipeline).

### Prior Auth (ePA) APIs [`2606`](https://hub.docker.com/r/healthsamurai/prior-auth)

Expand Down
Loading
Loading