From 068aa019171baa6374dbfedb7afa6e5889b36d33 Mon Sep 17 00:00:00 2001 From: Sergey Zaborovsky Date: Thu, 2 Jul 2026 08:35:29 +0600 Subject: [PATCH 1/5] docs(auth): source caller org identity from UDAP hl7-b2b claim, not NPI Interop now resolves requesting/provider payer identity from the UDAP HL7 B2B Authorization Extension organization_id claim in the access token (extensions.hl7-b2b.organization_id, #), not from Client.details.identifier NPI. NPI is just one identifier system that can ride through the claim. Aidbox emits the extension on issued tokens automatically, so no extra configuration is needed. - authentication.md: drop NPI-from-Client block; add "HL7 B2B authorization extension (UDAP)" section with claim shape, spec link, and the operations that read it. - payer-to-payer.md: add caller-identity row; update $bulk-member-match 403 rejection wording. - davinci-data-export.md: note payertopayer gates on the claim; add 403 error row. - bulk-member-match.md: claim-based identity in Auth, output prose, consent persistence, and 403/409 errors. - provider-member-match.md: caller identified by the claim. --- docs/api-reference/authentication.md | 30 ++++++++++++++++--- .../operations/bulk-member-match.md | 10 +++---- .../operations/davinci-data-export.md | 3 ++ .../operations/provider-member-match.md | 2 +- docs/interop-apis/payer-to-payer.md | 7 +++-- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/docs/api-reference/authentication.md b/docs/api-reference/authentication.md index e2b802b..16a8a37 100644 --- a/docs/api-reference/authentication.md +++ b/docs/api-reference/authentication.md @@ -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": ""}] + "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 `#` 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 diff --git a/docs/api-reference/operations/bulk-member-match.md b/docs/api-reference/operations/bulk-member-match.md index 9cd65bc..f46bcfa 100644 --- a/docs/api-reference/operations/bulk-member-match.md +++ b/docs/api-reference/operations/bulk-member-match.md @@ -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 @@ -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/` 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/` 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 @@ -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=&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. @@ -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.status = cancelled`, or caller is not the originating requester | | 404 | Cancel | Unknown `` (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) | diff --git a/docs/api-reference/operations/davinci-data-export.md b/docs/api-reference/operations/davinci-data-export.md index 1356ca3..853bff3 100644 --- a/docs/api-reference/operations/davinci-data-export.md +++ b/docs/api-reference/operations/davinci-data-export.md @@ -14,6 +14,8 @@ The operation is **always asynchronous** and follows the [FHIR Bulk Data export 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); a non-admin caller without it gets `403`. + ## Kick-off ### Endpoint @@ -270,6 +272,7 @@ The `payertopayer` **Data scope** above — the five-year window and the drug-PA | 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` | +| 403 | Kick-off (interop) | `payertopayer` export whose access token lacks the UDAP HL7 B2B `organization_id` claim (non-admin caller) | | 404 | Kick-off (Aidbox) | `Group/` not found | | 404 | Status / cancel (Aidbox) | Unknown `` or job expired | | 422 | Kick-off (Aidbox) | Body fails `BulkExportProfile` validation (for example empty `parameter[]`) | diff --git a/docs/api-reference/operations/provider-member-match.md b/docs/api-reference/operations/provider-member-match.md index 81db8f5..ffca610 100644 --- a/docs/api-reference/operations/provider-member-match.md +++ b/docs/api-reference/operations/provider-member-match.md @@ -156,7 +156,7 @@ Content-Type: application/fhir+json ## Auth -SMART Backend Services. See [Authentication](../authentication.md). +SMART Backend Services. Caller identity comes from the [UDAP HL7 B2B `organization_id` claim](../authentication.md#hl7-b2b-authorization-extension-udap). ## Kick-off diff --git a/docs/interop-apis/payer-to-payer.md b/docs/interop-apis/payer-to-payer.md index f1f69f3..458ea9f 100644 --- a/docs/interop-apis/payer-to-payer.md +++ b/docs/interop-apis/payer-to-payer.md @@ -16,11 +16,12 @@ The Payer-to-Payer API lets a receiving payer (new plan) pull a member's clinica |---|---| | Caller | Receiving payer (the new plan) | | Authentication | SMART Backend Services Authorization (asymmetric JWT, system-level scope) | +| Caller identity | UDAP HL7 B2B `organization_id` claim in the access token | | Token endpoint | `/auth/token` on the responding payer's deployment | -The two payers exchange JWKS endpoints out-of-band (or pre-shared keys) at onboarding time. +The two payers exchange JWKS endpoints out-of-band (or pre-shared keys) at onboarding time. The caller's `organization_id` claim gates the opt-in export and is matched against the submitted `Consent` recipient (`provision.actor[role=IRCP]`). -See [API Reference / Authentication](../api-reference/authentication.md). +See [API Reference / Authentication](../api-reference/authentication.md#hl7-b2b-authorization-extension-udap). ## Consent @@ -247,7 +248,7 @@ Three buckets in the response: | `NonMatchedMembers` | [`pdex-member-no-match-group`](https://build.fhir.org/ig/HL7/davinci-epdx/StructureDefinition-pdex-member-no-match-group.html) | No match found | | `ConsentConstrainedMembers` | [`pdex-member-no-match-group`](https://build.fhir.org/ig/HL7/davinci-epdx/StructureDefinition-pdex-member-no-match-group.html) (code `consentconstraint`) | Matched member with active opt-out, expired consent, or consent that could not be persisted | -`$bulk-member-match` rejects callers whose requesting-payer identity cannot be resolved from the OAuth client with `403`. +`$bulk-member-match` rejects non-admin callers whose access token lacks the hl7-b2b `organization_id` claim with `403`. See [API Reference / Operations / $bulk-member-match](../api-reference/operations/bulk-member-match.md). From 1253430c07b9ca3596bb8118d24a8664891b778f Mon Sep 17 00:00:00 2001 From: Sergey Zaborovsky Date: Thu, 2 Jul 2026 08:52:00 +0600 Subject: [PATCH 2/5] docs(export): reflect enforced $davinci-data-export scope filters The interop app now enforces server-side scope on $davinci-data-export kick-off (interop commits bdd04b3, 0e6f97c, 4204cd7), so the "not yet enforced" hints were stale. Update reference + pillars to match impl: - exportType values: mark provider-download and payertopayer as the only translated types; provider-delta / provider-snapshot return 422. - Document server-applied filters: opt-out/opt-in consent params, pharmacy (drug claim + drug PA) _typeFilter exclusion, payertopayer 5-year service-date floor on EOB, and the money-field _elements strip on ExplanationOfBenefit / Coverage (needs Aidbox nested-elements setting). - Fix the payertopayer auth gate status: 401 (login), not 403. - Add missing 400 diagnostics (exportType required, bad _outputFormat, caller EOB _typeFilter) and the interop 422 deferred-exportType row. - Note the remaining gaps: denied PAs not filtered; filtered resources carry SUBSETTED rather than CARIN BB Non-Financial Basis profiles. Payer-to-Payer and Provider Access pillars: flip the "not enforced" hints to enforced-with-gaps, cross-linking the reference filters section. --- .../operations/davinci-data-export.md | 55 ++++++++++++++----- docs/get-started/quickstart-run-locally.md | 1 + docs/interop-apis/payer-to-payer.md | 2 +- docs/interop-apis/provider-access.md | 4 ++ 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/docs/api-reference/operations/davinci-data-export.md b/docs/api-reference/operations/davinci-data-export.md index 853bff3..dc24983 100644 --- a/docs/api-reference/operations/davinci-data-export.md +++ b/docs/api-reference/operations/davinci-data-export.md @@ -8,13 +8,20 @@ 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); a non-admin caller without it gets `403`. +`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 @@ -35,11 +42,11 @@ The request body must be a FHIR `Parameters` resource. Unsupported parameter nam Dir.ParameterTypeCard.Description -INexportTypecanonical (valueCanonical)0..1One of hl7.fhir.us.davinci-pdex#provider-download, hl7.fhir.us.davinci-pdex#provider-delta, hl7.fhir.us.davinci-pdex#provider-snapshot, hl7.fhir.us.davinci-pdex#payertopayer. Selects the PDex scenario (consent rule + data scope) — see exportType values. -IN_sinceinstant (valueInstant)0..1Only resources updated since this time. Required when exportType = hl7.fhir.us.davinci-pdex#provider-delta. +INexportTypecanonical (valueCanonical)0..1Selects the PDex scenario (consent rule + data scope). Supported values: hl7.fhir.us.davinci-pdex#provider-download and hl7.fhir.us.davinci-pdex#payertopayer. See exportType values. +IN_sinceinstant (valueInstant)0..1Only resources updated since this time. IN_untilinstant (valueInstant)0..1Only resources updated up to this time. IN_typestring (valueString)0..1Comma-separated FHIR resource types to include (e.g. Patient,Coverage,ExplanationOfBenefit). -IN_typeFilterstring (valueString)0..*Per-type FHIR search expression (e.g. Observation?category=laboratory). +IN_typeFilterstring (valueString)0..*Per-type FHIR search expression (e.g. Observation?category=laboratory). A caller clause naming ExplanationOfBenefit is rejected with 400: it would OR-merge with the interop app's injected pharmacy/service-date EOB filter and re-admit excluded data. IN_outputFormatstring (valueString)0..1Output format. application/fhir+ndjson is the default and only value supported in PDex 2.1.0. INpatientreference (valueReference)0..*Narrow the export to specific Patients. OUT———202 Accepted with Content-Location pointing at the Aidbox status URL. @@ -96,10 +103,27 @@ Interop returns one of these diagnostics: - `Request body must be a FHIR Parameters resource` - `Unsupported parameter(s): ` +- `Parameter exportType is required` - `Parameter exportType must appear at most once` - `Parameter exportType must use valueCanonical` - `Unsupported exportType: ` -- `exportType provider-delta requires _since` +- `_outputFormat must be application/fhir+ndjson, got: ` +- `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 @@ -258,24 +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` | -| 403 | Kick-off (interop) | `payertopayer` export whose access token lacks the UDAP HL7 B2B `organization_id` claim (non-admin caller) | +| 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/` not found | | 404 | Status / cancel (Aidbox) | Unknown `` 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. diff --git a/docs/get-started/quickstart-run-locally.md b/docs/get-started/quickstart-run-locally.md index 646e29d..90f220d 100644 --- a/docs/get-started/quickstart-run-locally.md +++ b/docs/get-started/quickstart-run-locally.md @@ -123,6 +123,7 @@ services: BOX_FHIR_BULK_STORAGE_PROVIDER: aws BOX_FHIR_BULK_STORAGE_AWS_ACCOUNT: minio BOX_FHIR_BULK_STORAGE_AWS_BUCKET: aidbox-bulk + BOX_FHIR_BULK_DATA_EXPORT_NESTED_ELEMENTS: "true" BOX_SECURITY_AUDIT_LOG_ENABLED: "true" BOX_SECURITY_DEV_MODE: "true" BOX_SECURITY_ORGBAC_ENABLED: "true" diff --git a/docs/interop-apis/payer-to-payer.md b/docs/interop-apis/payer-to-payer.md index 458ea9f..e106c72 100644 --- a/docs/interop-apis/payer-to-payer.md +++ b/docs/interop-apis/payer-to-payer.md @@ -40,7 +40,7 @@ Five-year window of date-of-service, excluding remittances, cost-sharing, drug p | Prior authorization request and decision (excluding drug PAs and denied PAs) | ExplanationOfBenefit (`use=preauthorization`) | PDex 2.1.0 | {% hint style="info" %} -The five-year date-of-service window, the remittance / cost-sharing / drug-PA / denied-PA exclusions, and the CARIN BB Non-Financial Basis profiling above describe the target data scope; they are not yet enforced server-side. The export currently returns the Group's full resource set, so narrow it explicitly with `_type` / `_typeFilter` / `_since`. +The export enforces most of this scope server-side: `ExplanationOfBenefit` is floored to a 5-year `service-date` window, drug claims and drug PAs are dropped, and remittance / cost-sharing money fields are stripped from `ExplanationOfBenefit` and `Coverage`. Two gaps remain: denied prior authorizations are not yet filtered, and filtered resources carry Aidbox's `SUBSETTED` tag rather than the CARIN BB Non-Financial Basis profiles. Non-EOB clinical resources are date-bounded only if you pass `_since`. See [`$davinci-data-export`](../api-reference/operations/davinci-data-export.md#server-applied-filters). {% endhint %} ## Test dataset diff --git a/docs/interop-apis/provider-access.md b/docs/interop-apis/provider-access.md index 6ce4b29..069c6a7 100644 --- a/docs/interop-apis/provider-access.md +++ b/docs/interop-apis/provider-access.md @@ -89,6 +89,10 @@ Same data set Provider Access shares with Patient Access, **excluding provider r Service date floor: **January 1, 2016**. +{% hint style="info" %} +Money fields are stripped from `ExplanationOfBenefit` and `Coverage` at the field level, and drug claims / drug PAs are dropped whole. Filtered resources carry Aidbox's `SUBSETTED` tag rather than being re-tagged to the CARIN BB Non-Financial Basis profiles. See [`$davinci-data-export`](../api-reference/operations/davinci-data-export.md#server-applied-filters). +{% endhint %} + ## Operations ### `$provider-member-match` From 148e36ab32c861c9cfe9db0a3dcc1b29248b2336 Mon Sep 17 00:00:00 2001 From: Sergey Zaborovsky Date: Thu, 2 Jul 2026 09:34:57 +0600 Subject: [PATCH 3/5] docs(pas): document claim-response-reference extension on submitted Claim $submit links the persisted Claim to its ClaimResponse via a claim-response-reference extension, so the id rides along in change notifications and consumers correlate without a separate lookup. Add a ClaimResponse correlation section to the Claim/$submit reference and cross-reference it from Event Notifications. --- docs/api-reference/operations/claim-submit.md | 13 +++++++++++++ docs/prior-auth/event-notifications.md | 2 ++ 2 files changed, 15 insertions(+) diff --git a/docs/api-reference/operations/claim-submit.md b/docs/api-reference/operations/claim-submit.md index d4f31df..5bfb7d2 100644 --- a/docs/api-reference/operations/claim-submit.md +++ b/docs/api-reference/operations/claim-submit.md @@ -186,6 +186,19 @@ 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. diff --git a/docs/prior-auth/event-notifications.md b/docs/prior-auth/event-notifications.md index 0bc0ec4..04322aa 100644 --- a/docs/prior-auth/event-notifications.md +++ b/docs/prior-auth/event-notifications.md @@ -128,6 +128,8 @@ On create, confirm the subscription reached `active` (`GET /Subscription/pas-cla With `backport-payload-content: full-resource`, each delivery carries the complete triggering resource. For Prior Auth this is the `ClaimResponse` whose `reviewAction` extension conveys the decision (e.g. X12 `A1` = certified, `A3` = not certified, `A4` = pended — see [PAS](pas.md)). A subscriber that needs the full referenced context (Claim, Patient, Coverage) can resolve those references against the FHIR API, or use the AWS SNS extension below, which can ship them pre-resolved. +A subscriber that triggers on `Claim` instead finds the `ClaimResponse` id in the `claim-response-reference` extension Payerbox adds to the stored `Claim` (see [Claim/$submit](../api-reference/operations/claim-submit.md#claimresponse-link)), so it correlates the pair without a separate lookup. + ## Provisioning at deploy time In production these resources are created from an init-bundle rather than by hand, with environment-variable substitution for environment-specific values (endpoint URL, auth token). One ordering rule applies: a subscription topic must exist before any `Subscription` that references its `url`. From 7a645341f408e12c823aca33837d5f81da800c84 Mon Sep 17 00:00:00 2001 From: Sergey Zaborovsky Date: Thu, 2 Jul 2026 09:42:45 +0600 Subject: [PATCH 4/5] docs(pas): document 2.1.0 ClaimResponse-on-original update-chain resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $inquire walks Claim.related backward to the chain root and returns that Claim's ClaimResponse — under 2.1.0 updates/cancels carry none of their own. Note prior/replaces relationship codes and that an update $submit returns the original Claim's ClaimResponse. --- docs/api-reference/operations/claim-inquire.md | 10 ++++++++++ docs/api-reference/operations/claim-submit.md | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/api-reference/operations/claim-inquire.md b/docs/api-reference/operations/claim-inquire.md index 35d2f37..31facac 100644 --- a/docs/api-reference/operations/claim-inquire.md +++ b/docs/api-reference/operations/claim-inquire.md @@ -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). diff --git a/docs/api-reference/operations/claim-submit.md b/docs/api-reference/operations/claim-submit.md index 5bfb7d2..6f403c7 100644 --- a/docs/api-reference/operations/claim-submit.md +++ b/docs/api-reference/operations/claim-submit.md @@ -203,4 +203,4 @@ The id rides along on the `Claim` in [change notifications](../../prior-auth/eve 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. From f1d7c467eaf61a6c5f107c66124191fedd2f6567 Mon Sep 17 00:00:00 2001 From: Sergey Zaborovsky Date: Thu, 2 Jul 2026 10:26:56 +0600 Subject: [PATCH 5/5] docs(portal): split Developer Portal into SMART App and Backend Services pages Developer Portal page becomes a hub (account signup, client-type choice). Add SMART App and Backend Services child pages, each self-contained with its own registration, submission, and status. Backend Services documents the JWKS-URI vs client-secret auth choice, system scopes, and sandbox vs production. Update SUMMARY nav, redirects for moved anchors, and releases links. --- SUMMARY.md | 2 + docs/fhir-app-portal/backend-services.md | 45 +++++++++++ docs/fhir-app-portal/developer-portal.md | 95 +++++------------------- docs/fhir-app-portal/smart-app.md | 82 ++++++++++++++++++++ docs/releases.md | 6 +- redirects.yaml | 5 ++ 6 files changed, 156 insertions(+), 79 deletions(-) create mode 100644 docs/fhir-app-portal/backend-services.md create mode 100644 docs/fhir-app-portal/smart-app.md diff --git a/SUMMARY.md b/SUMMARY.md index a046b7e..3382c23 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -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) diff --git a/docs/fhir-app-portal/backend-services.md b/docs/fhir-app-portal/backend-services.md new file mode 100644 index 0000000..950e95c --- /dev/null +++ b/docs/fhir-app-portal/backend-services.md @@ -0,0 +1,45 @@ +--- +description: >- + Register a non-interactive Backend Services client (JWKS URI or client + secret, system scopes) in the Developer Portal. +--- + +# Backend Services + +A Backend Services client is non-interactive: a server authenticates with the `client_credentials` grant, no user involved. Use it for bulk export and server-to-server access. Register one from the **Backend Services** section of the [Developer Portal](developer-portal.md) dashboard. + +## Register a Backend Service + +1. From the dashboard, click **Register New** under **Backend Services** +2. Fill in the service details +3. Pick an **Authentication** method: + * **JWKS URI**: host a public JWKS; the service signs a `private_key_jwt` assertion. The SMART Backend Services method, required for ONC G10 / certified bulk data. + * **Client secret**: the portal generates a `client_id` + `client_secret`, shown once. Simpler, but not SMART Backend Services / G10 compliant. Rotate it from the service's detail page. +4. Set the **system scopes**: the subject is always `System`. Choose scope version (v1 or v2), FHIR resource, and operations (Create, Read, Update, Delete, Search). Produces scope strings like `system/Patient.rs` or `system/*.read`. +5. In multi-organization (OrgBAC) deployments, select the **Organization** whose data the service may access +6. Click **Create** + +Token exchange: [Authentication](../api-reference/authentication.md). + +## Sandbox and production + +Draft and under-review services run against the sandbox (developer Aidbox, sample data). On approval the client is copied to production (Payerbox PHI Aidbox) and its access policy is derived from the scopes. The service's detail page shows the environment constants (FHIR base URL, SMART configuration, token URL, capability statement); production URLs appear once approved. + +## Submit for review + +When the service is ready: + +1. Open the service's draft page +2. Click **Submit for Review** +3. Status changes to **Under Review** + +Administrators review it from the [Admin Portal](admin-portal.md) and approve or reject. On approval the service moves to production ([Sandbox and production](#sandbox-and-production)). + +## Service status + +| Status | Description | +|---|---| +| **Draft** | Created, not yet submitted | +| **Under Review** | Submitted, waiting for admin decision | +| **Active** | Approved and live in production | +| **Rejected** | Declined at review, or deactivated after going live | diff --git a/docs/fhir-app-portal/developer-portal.md b/docs/fhir-app-portal/developer-portal.md index b81eead..e6eab1e 100644 --- a/docs/fhir-app-portal/developer-portal.md +++ b/docs/fhir-app-portal/developer-portal.md @@ -1,23 +1,23 @@ --- description: >- - Developer guide to registering, testing, and submitting SMART on FHIR - applications in the Developer Portal. + Register a developer account, choose a client type, and submit clients for + review in the Developer Portal. --- # Developer Portal -The Developer Portal is a public-facing website where SMART on FHIR app developers can register, test, and manage their applications. +The Developer Portal is a public-facing website where developers register, test, and manage the clients that connect to Payerbox: interactive [SMART Apps](smart-app.md) and non-interactive [Backend Services](backend-services.md). ## Overview The Developer Portal enables: -* **Developer Registration** — Create an account to register apps -* **App Registration** — Register SMART Apps with OAuth settings -* **Test Launch** — Launch your app against sample patient data -* **App Submission** — Submit apps for admin review +* **Developer registration**: create an account to register clients +* **Client registration**: register a SMART App or a Backend Service +* **Sandbox testing**: run a client against sample data before submission +* **Submission**: send a client for admin review -## Register as a Developer +## Register as a developer 1. Open the Developer Portal and click **Sign Up** 2. Fill out the registration form @@ -25,74 +25,17 @@ The Developer Portal enables: 4. Set your password on the confirmation page 5. Click **Sign In** to access your dashboard -## Register a SMART App +## Choose a client type -1. From the dashboard, click **Register New** -2. Fill in the app details: - * **App name** — Your application name - * **Confidentiality** — Public or Confidential - * **Redirect URL** — Where users return after authorization - * **Launch URL** — Your app's SMART launch endpoint -3. Click **Create App** +The dashboard splits registration into two sections. Pick by how the client authenticates and who drives it. -You'll be redirected to your app's draft page where you can review, edit, or delete the app. +| | [SMART App](smart-app.md) | [Backend Services](backend-services.md) | +|---|---|---| +| Driven by | A user in a browser | A server, no user | +| OAuth grant | `authorization_code` | `client_credentials` | +| Register under | **Smart Apps** | **Backend Services** | +| Auth | Public or confidential; redirect + launch URL | JWKS URI or client secret | +| Scopes | `patient/*`, `user/*`, launch, OIDC | `system/*` only | +| Use for | Patient- or provider-facing apps | Bulk export, server-to-server APIs | -## Test Your App - -Before submitting for review, test your app in the sandbox: - -1. Copy the **Client ID** from your app's page -2. Configure your app to use this Client ID -3. Click **Test Launch** in the portal - -The test launch uses sample patient data (`Patient/test-pt-1`) and redirects to your launch URL with the required SMART context. - -## Submit for Review - -When your app is ready: - -1. Open your app's draft page -2. Click **Submit for Review** -3. Your app status changes to **Under Review** - -Administrators will review your app from the Admin Portal and either approve or reject it. - -## App Status - -| Status | Description | -| ---------------- | ---------------------------------- | -| **Draft** | Not yet submitted | -| **Under Review** | Waiting for admin decision | -| **Active** | Approved and available to patients | -| **Rejected** | Did not meet requirements | - -## Example: Growth Chart App - -Here's how to test with the Growth Chart demo app: - -### 1. Get the app - -```bash -git clone git@github.com:smart-on-fhir/growth-chart-app.git -cd growth-chart-app -npm install -npm start -``` - -### 2. Register in the portal - -Register a new app with: - -* **App name**: Growth Chart -* **Confidentiality**: Public -* **Redirect URL**: `http://localhost:9000/` -* **Launch URL**: `http://localhost:9000/launch.html` - -### 3. Configure and launch - -1. Copy the **Client ID** from the portal -2. Open `growth-chart-app/launch.html` and set the `client_id` value -3. Save the file -4. Click **Test Launch** in the portal - -The app will launch with test patient data. +Each client-type page covers its own registration, testing, submission, and status. diff --git a/docs/fhir-app-portal/smart-app.md b/docs/fhir-app-portal/smart-app.md new file mode 100644 index 0000000..e2f4325 --- /dev/null +++ b/docs/fhir-app-portal/smart-app.md @@ -0,0 +1,82 @@ +--- +description: >- + Register, test, and submit an interactive SMART on FHIR application in the + Developer Portal. +--- + +# SMART App + +A SMART App is an interactive SMART on FHIR client: a user launches it in a browser and it authenticates with the `authorization_code` grant. Register one from the **Smart Apps** section of the [Developer Portal](developer-portal.md) dashboard. + +## Register a SMART App + +1. From the dashboard, click **Register New** under **Smart Apps** +2. Fill in the app details: + * **App name**: your application name + * **Confidentiality**: public or confidential; a confidential client is issued a secret + * **Redirect URL**: where users return after authorization + * **Launch URL**: your app's SMART launch endpoint +3. Set the **SMART on FHIR scopes** the app requests (`patient/*`, `user/*`, plus launch and OIDC scopes) +4. Click **Create App** + +You're redirected to the app's draft page to review, edit, or delete it. + +## Test your app + +Before submitting, test the app in the sandbox: + +1. Copy the **Client ID** from the app's page +2. Configure your app to use this Client ID +3. Click **Test Launch** in the portal + +The test launch uses sample patient data (`Patient/test-pt-1`) and redirects to your launch URL with the required SMART context. + +## Submit for review + +When the app is ready: + +1. Open the app's draft page +2. Click **Submit for Review** +3. Status changes to **Under Review** + +Administrators review it from the [Admin Portal](admin-portal.md) and approve or reject. + +## App status + +| Status | Description | +|---|---| +| **Draft** | Created, not yet submitted | +| **Under Review** | Submitted, waiting for admin decision | +| **Active** | Approved and live in the [FHIR App Gallery](fhir-app-gallery.md) | +| **Rejected** | Declined at review, or deactivated after going live | + +## Example: Growth Chart app + +Here's how to test with the Growth Chart demo app. + +### 1. Get the app + +```bash +git clone git@github.com:smart-on-fhir/growth-chart-app.git +cd growth-chart-app +npm install +npm start +``` + +### 2. Register in the portal + +Register a new app with: + +* **App name**: Growth Chart +* **Confidentiality**: Public +* **Redirect URL**: `http://localhost:9000/` +* **Launch URL**: `http://localhost:9000/launch.html` + +### 3. Configure and launch + +1. Copy the **Client ID** from the portal +2. Open `growth-chart-app/launch.html` and set the `client_id` value +3. Save the file +4. Click **Test Launch** in the portal + +The app launches with test patient data. diff --git a/docs/releases.md b/docs/releases.md index 59d2570..1f0958e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -43,7 +43,7 @@ A new `payerbox` umbrella Helm chart deploys the full stack (portals, Interop AP **Developer Portal** -- Register a backend (bulk data) service with a client secret (client-credentials), in addition to a JWKS URI. See [Developer Portal](fhir-app-portal/developer-portal.md). +- Register a backend (bulk data) service with a client secret (client-credentials), in addition to a JWKS URI. See [Backend Services](fhir-app-portal/backend-services.md). **Admin Portal** @@ -93,8 +93,8 @@ A new `payerbox` umbrella Helm chart deploys the full stack (portals, Interop AP **Developer Portal** -- Register SMART apps with configurable scopes and supported search parameters, including DSI (decision-support intervention) transparency fields. See [Developer Portal](fhir-app-portal/developer-portal.md). -- Register backend (system) services for the bulk data APIs; these clients authenticate with a customer-supplied `jwks_uri` (JWKS URL) rather than a client secret. See [Authentication](api-reference/authentication.md). +- Register SMART apps with configurable scopes and supported search parameters, including DSI (decision-support intervention) transparency fields. See [SMART App](fhir-app-portal/smart-app.md). +- Register backend (system) services for the bulk data APIs; these clients authenticate with a customer-supplied `jwks_uri` (JWKS URL) rather than a client secret. See [Backend Services](fhir-app-portal/backend-services.md). **Admin Portal** diff --git a/redirects.yaml b/redirects.yaml index 172f056..b4ea465 100644 --- a/redirects.yaml +++ b/redirects.yaml @@ -1,2 +1,7 @@ redirects: run-payerbox/maintain/event-notifications.md: prior-auth/event-notifications.md + fhir-app-portal/developer-portal#register-a-smart-app: fhir-app-portal/smart-app.md#register-a-smart-app + fhir-app-portal/developer-portal#test-your-app: fhir-app-portal/smart-app.md#test-your-app + fhir-app-portal/developer-portal#example-growth-chart-app: fhir-app-portal/smart-app.md#example-growth-chart-app + fhir-app-portal/developer-portal#submit-for-review: fhir-app-portal/smart-app.md#submit-for-review + fhir-app-portal/developer-portal#app-status: fhir-app-portal/smart-app.md#app-status