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
2 changes: 2 additions & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
* [Event Notifications](prior-auth/event-notifications.md)
* [FHIR App Portal](fhir-app-portal/README.md)
* [Developer Portal](fhir-app-portal/developer-portal.md)
* [SMART App](fhir-app-portal/smart-app.md)
* [Backend Services](fhir-app-portal/backend-services.md)
* [Admin Portal](fhir-app-portal/admin-portal.md)
* [FHIR App Gallery](fhir-app-portal/fhir-app-gallery.md)
* [Compliance](compliance/README.md)
Expand Down
30 changes: 26 additions & 4 deletions docs/api-reference/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,14 +241,36 @@ Content-Type: application/json
}
},
"jwks_uri": "https://partner.example.com/.well-known/jwks.json",
"scope": ["system/*.read"],
"details": {
"identifier": [{"system": "http://hl7.org/fhir/sid/us-npi", "value": "<NPI>"}]
"scope": ["system/*.read"]
}
```

### HL7 B2B authorization extension (UDAP)

Member-match and Payer-to-Payer operations identify the calling organization from the [UDAP HL7 B2B Authorization Extension](https://hl7.org/fhir/us/udap-security/b2b.html#b2b-authorization-extension-object), not from any `Client` field. Aidbox produces the `hl7-b2b` extension on issued access tokens automatically; no extra configuration is required. Payerbox reads it from the token JWT under `extensions.hl7-b2b`:

```json
{
"sub": "partner-payer",
"extensions": {
"hl7-b2b": {
"version": "1",
"organization_id": "http://hl7.org/fhir/sid/us-npi#1234567893",
"purpose_of_use": ["..."]
}
}
}
```

For operations that require the caller's NPI (`$bulk-member-match`, `$provider-member-match`), the NPI must be present on `Client.details.identifier[system=http://hl7.org/fhir/sid/us-npi]`. Aidbox rejects top-level `Client.identifier`, so the NPI lives under `details`.
`organization_id` is a single string in `<system-uri>#<value>` form, split on the last `#`. Any identifier system works: NPI, NAIC, or CARIN BB `payerid`. Operations that read it:

| Operation | Uses `organization_id` for |
|---|---|
| [`$bulk-member-match`](operations/bulk-member-match.md) | Requesting-payer identity; stamped onto `MatchedMembers`; matched against Consent `provision.actor[role=IRCP]` |
| [`$provider-member-match`](operations/provider-member-match.md) | Caller identity |
| [`$davinci-data-export`](operations/davinci-data-export.md) (`payertopayer`) | Gates the opt-in export and resolves the requesting payer |

A non-admin caller without the claim gets `403`.

### Client assertion JWT

Expand Down
10 changes: 5 additions & 5 deletions docs/api-reference/operations/bulk-member-match.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ Content-Type: application/fhir+json

## Auth

SMART Backend Services. See [Authentication](../authentication.md).
SMART Backend Services. Requesting-payer identity comes from the [UDAP HL7 B2B `organization_id` claim](../authentication.md#hl7-b2b-authorization-extension-udap).

## Kick-off

Expand Down Expand Up @@ -513,7 +513,7 @@ Body (single ndjson line, formatted for readability):
{% endtab %}
{% endtabs %}

`MatchedMembers` and `ConsentConstrainedMembers` carry the requesting payer's NPI in `characteristic[0].valueReference.identifier`; the literal `Organization/<id>` reference is added when the responding payer has an Organization registered for that NPI. `Patient.identifier` entries submitted on `MemberPatient` are used as additional matching tokens but are not echoed back on the matched Group.
`MatchedMembers` and `ConsentConstrainedMembers` carry the requesting payer's identifier in `characteristic[0].valueReference.identifier`: the `{system, value}` parsed from the hl7-b2b `organization_id` claim. The literal `Organization/<id>` reference is added when the responding payer has an Organization registered with that identifier. The example uses an NPI system, but any registered system works. `Patient.identifier` entries submitted on `MemberPatient` are used as additional matching tokens but are not echoed back on the matched Group.

## Cancellation

Expand Down Expand Up @@ -568,7 +568,7 @@ Each submitted member is evaluated independently. Per-member failures never fail

The opt-out check reuses the same Aidbox search as [`$provider-member-match`](provider-member-match.md#matching-behavior): `Consent?patient=<id>&status=active&category=http://hl7.org/fhir/us/davinci-pdex/CodeSystem/pdex-consent-api-purpose|provider-access&provision-type=deny`.

**Consent persistence.** For each remaining matched member the submitted `Consent` is upserted into Aidbox with a deterministic id (`SHA-1(payer-org-id|patient-id)`); `Consent.patient` is rewritten to the matched payer Patient and `Consent.organization` to the requesting payer's Organization (FHIR shape is `0..*`; today exactly one element is written). The persisted Consent is what the later `$davinci-data-export?exportType=payertopayer` query reads against. If persistence fails — including the case where the requesting payer's NPI has no `Organization` registered in the responding payer's Aidbox — the member is re-bucketed to `ConsentConstrainedMembers`.
**Consent persistence.** For each remaining matched member the submitted `Consent` is upserted into Aidbox with a deterministic id (`SHA-1(payer-org-id|patient-id)`); `Consent.patient` is rewritten to the matched payer Patient and `Consent.organization` to the requesting payer's Organization (FHIR shape is `0..*`; today exactly one element is written). The persisted Consent is what the later `$davinci-data-export?exportType=payertopayer` query reads against. If persistence fails — including the case where the requesting payer's identifier has no `Organization` registered in the responding payer's Aidbox — the member is re-bucketed to `ConsentConstrainedMembers`.

**Stale Consent deactivation.** If a later `$bulk-member-match` for the same `(matched-patient, requesting-payer)` lands the member in `ConsentConstrainedMembers` (failed match-time check, opt-out hit, or persistence failure), the prior persisted `Consent` at the deterministic id is flipped to `status = inactive`. The row is retained for audit, but `$davinci-data-export?exportType=payertopayer` will not honor it on subsequent reads.

Expand All @@ -581,10 +581,10 @@ Output Groups carry no `period.end` and no TTL extension today. Until lifecycle
| Status | Where | Cause |
|---|---|---|
| 400 | Kick-off | `Prefer: respond-async` header missing |
| 403 | Kick-off | Requesting payer identity could not be resolved from the OAuth client |
| 403 | Kick-off | Access token lacks the UDAP HL7 B2B `organization_id` claim (non-admin caller) |
| 404 | Status / output | Unknown `<task-id>`, `Task.status = cancelled`, or caller is not the originating requester |
| 404 | Cancel | Unknown `<task-id>` (hard-deleted), or caller is not the originating requester (cancel on a `cancelled` Task returns `202` and sweeps outputs) |
| 409 | Kick-off | Requesting payer identity is ambiguous — more than one Organization in the responding payer's directory matches; resolve duplicates and retry |
| 409 | Kick-off | Requesting payer identity is ambiguous — more than one Organization in the responding payer's directory matches the claim's identifier; resolve duplicates and retry |
| 422 | Kick-off | Input `Parameters` failed `$validate` against the input profile |
| 500 | Kick-off | Failed to resolve requesting payer Organization (transient Aidbox read failure) |
| 500 | Status | Background processing failed; generic `OperationOutcome` returned (real cause in interop-app logs) |
Expand Down
10 changes: 10 additions & 0 deletions docs/api-reference/operations/claim-inquire.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,13 @@ Accept: application/json

{% endtab %}
{% endtabs %}

## Update chains

A prior authorization is updated or cancelled by submitting a new `Claim` that points at the prior one through `Claim.related` (relationship `prior`, or the legacy `replaces`). Under PAS 2.1.0 the decision `ClaimResponse` stays on the original (root) `Claim` of the chain; update and cancel `Claim`s carry none of their own.

From any `Claim` in the chain, `$inquire` walks `Claim.related` backward to the root and returns that `Claim`'s latest `ClaimResponse`. Inquiring on the original `Claim`, an intermediate update, or the newest update returns the same `ClaimResponse`. Cycle and depth guards stop a malformed chain from looping.

Under the legacy 2.0.1 model each `Claim` in a chain carried its own `ClaimResponse`, and `$inquire` resolved forward to the newest one. Payerbox behavior, not a PAS profile element.

See [Claim/$submit](claim-submit.md#claimresponse-link), [PAS](../../prior-auth/pas.md).
15 changes: 14 additions & 1 deletion docs/api-reference/operations/claim-submit.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,21 @@ Accept: application/json
{% endtab %}
{% endtabs %}

## ClaimResponse link

Before persisting, Payerbox links the `Claim` to its `ClaimResponse`: the stored `Claim` carries an extension whose `valueReference` points at the `ClaimResponse`.

```json
{
"url": "https://fhir.aidbox.app/fhir/StructureDefinition/claim-response-reference",
"valueReference": { "reference": "ClaimResponse/c0d73c37-12ee-4cde-bfc6-aa6ed216f4dd" }
}
```

The id rides along on the `Claim` in [change notifications](../../prior-auth/event-notifications.md), so a consumer correlates the pair without a separate `ClaimResponse` search. Present on initial submits and on PAS 2.1.0 update/cancel requests, which reuse the prior authorization's original `ClaimResponse` instead of minting a new one. Payerbox behavior, not a PAS profile element.

## Duplicate submissions

A submission is matched against existing claims by the first `Claim.identifier` (its `system` and `value`). If a claim with the same identifier already exists, `$submit` returns its latest `ClaimResponse` and creates nothing new. This makes retries safe: resending the same bundle does not fork the authorization into a second record.

Matching uses only the identifier. Changed `item[]` content does not create a new claim; to change a prior authorization, submit a new `Claim.identifier` with `Claim.related.relationship = "replaces"` pointing to the previous `Claim`. Identifiers without both `system` and `value` are not matched and always create a new claim.
Matching uses only the identifier. Changed `item[]` content does not create a new claim; to change a prior authorization, submit a new `Claim` (new `Claim.identifier`) whose `Claim.related` points at the previous `Claim` with relationship `prior` (PAS 2.1.0; the legacy `replaces` code is also accepted). An update `$submit` returns the [original Claim's `ClaimResponse`](#claimresponse-link) rather than minting a new one. Identifiers without both `system` and `value` are not matched and always create a new claim.
54 changes: 41 additions & 13 deletions docs/api-reference/operations/davinci-data-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@ Exports clinical, claims, encounter, and prior-authorization data for the member

The operation is **always asynchronous** and follows the [FHIR Bulk Data export pattern](https://hl7.org/fhir/uv/bulkdata/export.html): kick-off returns `202 Accepted` with `Content-Location`, the client polls the status URL, and downloads each ndjson file referenced in the completed manifest.

`exportType` selects the PDex scenario the request runs under and governs consent rules and data scope per the IG (see [`exportType` values](#exporttype-values)). The kick-off endpoint is owned by Payerbox's interop app; status polling, output download, and cancellation are served by Aidbox at its standard Bulk Data Export URLs (`$export-status`, signed output URLs).
`exportType` selects the PDex scenario the request runs under (see [`exportType` values](#exporttype-values)). Scope follows the Da Vinci [Payer-to-Payer Bulk Data Exchange](https://build.fhir.org/ig/HL7/davinci-epdx/payertopayerbulkexchange.html) and [ATR `$davinci-data-export` requirements](https://hl7.org/fhir/us/davinci-atr/STU2.1/spec.html#requirements-for-implementation-of-the-davinci-data-export-operation):

- Opt-in / opt-out consent, per exportType
- Financial fields excluded from `ExplanationOfBenefit` and `Coverage`
- Drug claims and drug PAs dropped
- 5-year `ExplanationOfBenefit` `service-date` window for `payertopayer`

The kick-off endpoint is owned by Payerbox's interop app; status polling, output download, and cancellation are served by Aidbox at its standard Bulk Data Export URLs (`$export-status`, signed output URLs).

## Auth

SMART Backend Services and Client Credentials. The caller's OAuth client must be allowed to read the Group and the resource types referenced by `_type` (and their referenced resources, since Aidbox `$export` chases references). See [Authentication](../authentication.md).

`payertopayer` gates the opt-in export on the [UDAP HL7 B2B `organization_id` claim](../authentication.md#hl7-b2b-authorization-extension-udap); the interop app fails closed with `401` when the caller's token carries no such claim.

## Kick-off

### Endpoint
Expand All @@ -33,11 +42,11 @@ The request body must be a FHIR `Parameters` resource. Unsupported parameter nam
<tr><th>Dir.</th><th width="150">Parameter</th><th width="190">Type</th><th>Card.</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td>IN</td><td><code>exportType</code></td><td>canonical (<code>valueCanonical</code>)</td><td>0..1</td><td>One of <code>hl7.fhir.us.davinci-pdex#provider-download</code>, <code>hl7.fhir.us.davinci-pdex#provider-delta</code>, <code>hl7.fhir.us.davinci-pdex#provider-snapshot</code>, <code>hl7.fhir.us.davinci-pdex#payertopayer</code>. Selects the PDex scenario (consent rule + data scope) — see <a href="#exporttype-values"><code>exportType</code> values</a>.</td></tr>
<tr><td>IN</td><td><code>_since</code></td><td>instant (<code>valueInstant</code>)</td><td>0..1</td><td>Only resources updated since this time. <strong>Required</strong> when <code>exportType = hl7.fhir.us.davinci-pdex#provider-delta</code>.</td></tr>
<tr><td>IN</td><td><code>exportType</code></td><td>canonical (<code>valueCanonical</code>)</td><td>0..1</td><td>Selects the PDex scenario (consent rule + data scope). Supported values: <code>hl7.fhir.us.davinci-pdex#provider-download</code> and <code>hl7.fhir.us.davinci-pdex#payertopayer</code>. See <a href="#exporttype-values"><code>exportType</code> values</a>.</td></tr>
<tr><td>IN</td><td><code>_since</code></td><td>instant (<code>valueInstant</code>)</td><td>0..1</td><td>Only resources updated since this time.</td></tr>
<tr><td>IN</td><td><code>_until</code></td><td>instant (<code>valueInstant</code>)</td><td>0..1</td><td>Only resources updated up to this time.</td></tr>
<tr><td>IN</td><td><code>_type</code></td><td>string (<code>valueString</code>)</td><td>0..1</td><td>Comma-separated FHIR resource types to include (e.g. <code>Patient,Coverage,ExplanationOfBenefit</code>).</td></tr>
<tr><td>IN</td><td><code>_typeFilter</code></td><td>string (<code>valueString</code>)</td><td>0..*</td><td>Per-type FHIR search expression (e.g. <code>Observation?category=laboratory</code>).</td></tr>
<tr><td>IN</td><td><code>_typeFilter</code></td><td>string (<code>valueString</code>)</td><td>0..*</td><td>Per-type FHIR search expression (e.g. <code>Observation?category=laboratory</code>). A caller clause naming <code>ExplanationOfBenefit</code> is rejected with <code>400</code>: it would OR-merge with the interop app's injected pharmacy/service-date EOB filter and re-admit excluded data.</td></tr>
<tr><td>IN</td><td><code>_outputFormat</code></td><td>string (<code>valueString</code>)</td><td>0..1</td><td>Output format. <code>application/fhir+ndjson</code> is the default and only value supported in PDex 2.1.0.</td></tr>
<tr><td>IN</td><td><code>patient</code></td><td>reference (<code>valueReference</code>)</td><td>0..*</td><td>Narrow the export to specific Patients.</td></tr>
<tr><td>OUT</td><td>—</td><td>—</td><td>—</td><td><code>202 Accepted</code> with <code>Content-Location</code> pointing at the Aidbox status URL.</td></tr>
Expand Down Expand Up @@ -94,10 +103,27 @@ Interop returns one of these diagnostics:

- `Request body must be a FHIR Parameters resource`
- `Unsupported parameter(s): <name>`
- `Parameter exportType is required`
- `Parameter exportType must appear at most once`
- `Parameter exportType must use valueCanonical`
- `Unsupported exportType: <value>`
- `exportType provider-delta requires _since`
- `_outputFormat must be application/fhir+ndjson, got: <value>`
- `Caller-supplied _typeFilter on ExplanationOfBenefit is not supported`
{% endtab %}
{% tab title="Response (missing UDAP claim)" %}
```http
HTTP/1.1 401 Unauthorized
Content-Type: application/fhir+json

{
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "login",
"diagnostics": "payertopayer export requires the caller UDAP hl7-b2b organization_id claim; it is missing"
}]
}
```
{% endtab %}
{% tab title="Response (group not found)" %}
```http
Expand Down Expand Up @@ -256,23 +282,25 @@ HTTP/1.1 202 Accepted

| Value | Used by | Consent rule | Data scope |
|---|---|---|---|
| `hl7.fhir.us.davinci-pdex#provider-download` | Provider Access | Member opt-out applies | Full set of resources for each member in the Group |
| `hl7.fhir.us.davinci-pdex#provider-delta` | Provider Access | Member opt-out applies | Only resources updated since `_since` (`_since` is required) |
| `hl7.fhir.us.davinci-pdex#provider-snapshot` | Provider Access | Member opt-out applies | Point-in-time snapshot of each member's record |
| `hl7.fhir.us.davinci-pdex#payertopayer` | Payer-to-Payer | Active opt-in `Consent` asserted at `$bulk-member-match` time | Five-year window; excludes drug PAs, denied PAs, provider remittances, and enrollee cost-sharing |
| `hl7.fhir.us.davinci-pdex#provider-download` | Provider Access | Member opt-out (`consentStrategy=opt-out`, PDex Provider Consent, `provision.type=deny`) | Full member record, minus drug claims / drug PAs and money fields |
| `hl7.fhir.us.davinci-pdex#payertopayer` | Payer-to-Payer | Active opt-in `Consent` asserted at `$bulk-member-match` time (`consentStrategy=opt-in`, HRex Consent) | Same as `provider-download`, plus `ExplanationOfBenefit` floored to a 5-year `service-date` window |

Denied PAs are not yet excluded, and a caller `_typeFilter` naming `ExplanationOfBenefit` is rejected with `400`.

{% hint style="info" %}
The `payertopayer` **Data scope** above — the five-year window and the drug-PA / denied-PA / remittance / cost-sharing exclusions — describes the target Da Vinci semantics; it is not yet enforced by the export. The export currently returns the Group's full resource set, so narrow it explicitly with `_type` / `_typeFilter` / `_since`.
Financial filtering requires Aidbox's `fhir.bulk-data.export.nested-elements` setting. The [Quickstart Docker Compose](../../get-started/quickstart-run-locally.md) turns it on via `BOX_FHIR_BULK_DATA_EXPORT_NESTED_ELEMENTS`; see [Aidbox nested `_elements`](https://www.health-samurai.io/docs/aidbox/api/bulk-api/export#nested-elements).
{% endhint %}

## Errors

| Status | Where | Cause |
|---|---|---|
| 400 | Kick-off (interop) | `Prefer: respond-async` missing; body not `Parameters`; unsupported parameter; unsupported `exportType`; `exportType` not using `valueCanonical`; `provider-delta` without `_since` |
| 400 | Kick-off (interop) | `Prefer: respond-async` missing; body not `Parameters`; unsupported parameter; `exportType` missing, repeated, not `valueCanonical`, or unknown; `_outputFormat` not `application/fhir+ndjson`; caller `_typeFilter` naming `ExplanationOfBenefit` |
| 401 | Kick-off (interop) | `payertopayer` export whose access token carries no UDAP HL7 B2B `organization_id` claim; the opt-in gate fails closed |
| 422 | Kick-off (interop) | `exportType` is a recognized Da Vinci canonical this server does not yet translate; Group `meta.profile` incompatible with `exportType`; inactive Group |
| 404 | Kick-off (Aidbox) | `Group/<id>` not found |
| 404 | Status / cancel (Aidbox) | Unknown `<job-id>` or job expired |
| 422 | Kick-off (Aidbox) | Body fails `BulkExportProfile` validation (for example empty `parameter[]`) |
| 500 | Status (Aidbox) | Background export failed see `BulkExportStatus.extension[BulkExportStatus.internal-error]` |
| 500 | Status (Aidbox) | Background export failed; see `BulkExportStatus.extension[BulkExportStatus.internal-error]` |

For architectural context see the [Provider Access](../../interop-apis/provider-access.md) and [Payer-to-Payer](../../interop-apis/payer-to-payer.md) pillars.
See the [Provider Access](../../interop-apis/provider-access.md) and [Payer-to-Payer](../../interop-apis/payer-to-payer.md) pillars.
Loading
Loading