feat(bcda): close the upstream-review gaps — partial exports, claim identity across systems, attribution checks, filter grammar, vendored behaviour
Some checks failed
CI / lint (push) Successful in 54s
CI / notebooks-smoke (push) Successful in 1m43s
CI / test (push) Failing after 2m56s
Infra CI / zotero (push) Successful in 15s
Infra CI / notebooks (push) Successful in 1m1s
Infra CI / api (push) Successful in 1m7s
Infra CI / docs (push) Successful in 1m50s
Infra CI / llm (push) Successful in 49s
Infra CI / mc (push) Failing after 35s
Infra CI / bcda (push) Successful in 1m42s
CI / lint (pull_request) Successful in 45s
CI / notebooks-smoke (pull_request) Successful in 1m38s
CI / test (pull_request) Failing after 2m53s
Infra CI / mc (pull_request) Failing after 30s
Infra CI / zotero (pull_request) Successful in 16s
Infra CI / notebooks (pull_request) Successful in 58s
Infra CI / docs (pull_request) Successful in 16s
Infra CI / api (pull_request) Successful in 15s
Infra CI / bcda (pull_request) Successful in 21s
Infra CI / llm (pull_request) Successful in 17s

Closes #1293 #1294 #1295 #1296 #1297 #1298 #1299 #1300 (upstream review
docs/superpowers/specs/2026-10-25-bcda-upstream-review.md).

Partial exports (#1293). The manifest's `error` list is downloaded as
OperationOutcome[.N].ndjson with resource_type=OperationOutcome; every
URL is attempted and failures are recorded per file, then raised once as
DownloadError. A job with error files is `partial`: it still reconciles
(its rows are real) but never becomes the watermark, check_since warns
`partial_job`, the Store page lists the issues and `stack bcda errors`
prints them.

Claim identity (#1294), per bcda.cms.gov "Matching multiple EOBs to the
same claim": three new header columns — clm_cntl_num_match (VMS leading 1
stripped on SharedSystem DME), clm_natural_key (Part D events without a
control number: bene · line from-date · provider qualifier · service
provider · RX number · fill number) and superseded_by — plus
prvdr_srvc_id_qlfyr_cd. bcda.sync keys claims on match → control number
→ natural key → id, marks every claim another current claim names in
`related` as superseded (chain walked), counts `superseded`, and a new
invariant forbids a live claim being named predecessor by another live
claim. The connector dedupes on the same identity and drops superseded
rows. Partitions and current tables flattened before these columns
existed get them as NULLs.

Attribution (#1295): `attribution_expired` (block) from
expiration_dates, `attribution_floor` (warn) from the earliest
last_attribution_update recorded on job rows (new column), and the
server's 404/400/429 texts explained by explain_server_error.

Adoption (#1296): completed_jobs() drops jobs older than the 24 h file
retention; downloads no longer stop at the first failed file.

_typeFilter (#1297): build_type_filter follows bcda/fhir/param.go —
outcome lists, service-date earliest and latest bounds with eq/gt/ge/lt/le
on YYYY[-MM[-DD]], no comma lists, no conflicting prefixes; CLI and
Exports page expose them.

Runout (#1298): `runout_window` warn after July; population column on
the Sync page ledger.

Vendoring (#1299): the manifest now pins bcda-app's param.go,
bluebutton.go, service.go, constants, models and prod.yml, the docs
source (CMSgov/bcda-static-site: filter/access/migrate/PAC/FAQ pages and
announcements) and the PAC and unmapped-fields dictionaries.

Docs (#1300): the reconciliation design record states the server's
window `(since, transactionTime]`, the lookback bound, the silent
exclusions, re-loads, partial exports and the identity rules.

refs #1139
This commit is contained in:
kert
2026-10-25 17:10:24 -04:00
parent 3dc44d0650
commit 2cc13008a9
147 changed files with 5899 additions and 198 deletions

View File

@@ -25,6 +25,35 @@ files = [
"shared_files/gzip_feature_test/test_gzip_encoded.ndjson",
"shared_files/gzip_feature_test/test_no_encoding.ndjson",
"conf/configs/sandbox.yml",
"conf/configs/prod.yml",
"bcda/fhir/param.go",
"bcda/client/bluebutton.go",
"bcda/service/service.go",
"bcda/constants/constants.go",
"bcda/constants/types.go",
"bcda/models/models.go",
]
[[source]]
name = "bcda-static-site"
kind = "github"
repo = "CMSgov/bcda-static-site"
ref = "main"
commit = "cd14babf8eca7247c35a6ba63ae5c221764ba236"
license = "LICENSE"
files = [
"LICENSE",
"src/_pages/api-documentation/filter-claims-data.md",
"src/_pages/api-documentation/access-claims-data.md",
"src/_pages/api-documentation/how-to-migrate-v3.md",
"src/_pages/bcda-data/partially-adjudicated-claims-data.md",
"src/_pages/bcda-data/comparison-bcda-cclf-files.md",
"src/_pages/support/faqs.md",
"src/_announcements/2025-py-data-limit-ended.md",
"src/_announcements/bcda-data-limited-in-jan-2026-for-yearly-updates.md",
"src/_announcements/bcda-launches-v3.md",
"src/_announcements/bcda-v3-expanded-access-general-availability.md",
"src/_announcements/bcda-performance-improvement-to-speed-up-job-requests.md",
]
[[source]]
@@ -34,6 +63,8 @@ base = "https://bcda.cms.gov/assets/downloads/"
files = [
"BCDA_v3_Data_Dictionary.xlsx",
"BCDA_v2_Data_Dictionary.xlsx",
"BCDA_Partially_Adjudicated_Data_Dictionary.xlsx",
"unmapped-fields-between-cclf-and-bcda.xlsx",
"v3_Patient.ndjson",
"v3_Coverage.ndjson",
"v3_ExplanationOfBenefit.ndjson",
@@ -44,10 +75,17 @@ files = [
"bcda-app/bcda/api/requests.go" = "3ae1b33dec6e0d9c2c93352f3da7a713ffeeda545e5be4d00643b43e65569456"
"bcda-app/bcda/api/v3/api.go" = "18a1dbc627d01a4d9f53113a6e45671494445da3fb8f9c877437f4ed8d62d871"
"bcda-app/bcda/api/v3/api_test.go" = "97c0ca1d0af2f5fe2fa68102b8c6d6d9709f2775ed58cd291886e62e66cd869c"
"bcda-app/bcda/client/bluebutton.go" = "e70841e241d3b1a8a6b9dff6d029fdfc28d87f007ca86a4e7e0e202d3b729d62"
"bcda-app/bcda/constants/constants.go" = "bbfbcaf66ed1e9d4cf8160a2d3b18c9cc38a47444868181e36643e3d8f74fdef"
"bcda-app/bcda/constants/types.go" = "dafa4f745ef57c71289d65e05b642dd9e165db98a53ec2f3e6865ce2c9bf2b8c"
"bcda-app/bcda/fhir/param.go" = "693c26317f01741bfc5bd62b7955438858459e607b37e3caa8a56bbff7088ada"
"bcda-app/bcda/models/models.go" = "496a4c0212de5a7f86930c7b00b8d880f89b6708b29da2e99d015d46ef3b25df"
"bcda-app/bcda/responseutils/v3/writer.go" = "d5bb9e8dfcad75bc3cf5cadf0f811e1b16d4e09cd0755970517ab3ab0867f56c"
"bcda-app/bcda/responseutils/v3/writer_test.go" = "9056a6f0c4e6dc747375e232a982064d3e616b9832baa90f94c7bbe241bd58bc"
"bcda-app/bcda/service/service.go" = "f98c770026ec19cea0543937dc402f8c59a4be5415fac9632925ef8e78465158"
"bcda-app/bcda/web/middleware/validation.go" = "dc4dfdf730c24961746f0b8f576b5a1cf912d7cec5e237401126738e6a82ed6a"
"bcda-app/bcda/web/middleware/validation_test.go" = "c16f268ae3186b6881aa06ef1374d93117a969e87eafd13edcde0ac766a6b5e1"
"bcda-app/conf/configs/prod.yml" = "e644db4b609ce3f72a2b281db6a1bd24fe9648e8013a1de570710500173f0bc5"
"bcda-app/conf/configs/sandbox.yml" = "a954b23194796658ddcfc8282ef3de488356a6076ed84266cfce6959df68e7f5"
"bcda-app/shared_files/gzip_feature_test/corrupt_gz_file.ndjson" = "41b355a94e96ef4ed92c6fde19ca67d7341a5f95bd67e6b1e8a65f1b83be2b36"
"bcda-app/shared_files/gzip_feature_test/test_gzip_encoded.ndjson" = "b5a7caf480fec615a1487f7bde4974f373395d98f245598c146844b1003f9a8f"
@@ -55,8 +93,22 @@ files = [
"bcda-app/swagger.json" = "3565bdea5a5f0817b3abb95432645ef15e32179dbc18fc2a4d3de434d293e03f"
"bcda-app/test/postman_test/BCDA_V3_Tests.postman_collection.json" = "fbb0c523f078f215bee178a9010fd331287c3b52ea6d712ea328e392ed77c237"
"bcda-app/test/postman_test/sandbox.postman_environment.json" = "6d1866a8ae06185390ff80180bff0c38d93f591d71066d51b6a1f3a80cf6377c"
"bcda-site/BCDA_Partially_Adjudicated_Data_Dictionary.xlsx" = "9451a3988d7d1c8c3375c5a64c370bd19f35abc0febe3c0c8d36927d4bb1f233"
"bcda-site/BCDA_v2_Data_Dictionary.xlsx" = "9c91eae07869f573c5578da9606336b00f1768180a947e2c4275864877f88810"
"bcda-site/BCDA_v3_Data_Dictionary.xlsx" = "335547374a66ce88b4c88cf0657ec09f5e731af6b7ede998e0b162f6594ec3bc"
"bcda-site/unmapped-fields-between-cclf-and-bcda.xlsx" = "9818933895ce58bed27f9208758f7edd0594a6f6bc48770726b8f89c6af01313"
"bcda-site/v3_Coverage.ndjson" = "cbe8820255f7a15d896b9098a33422d1e012cacc059316ac4894272132ff2986"
"bcda-site/v3_ExplanationOfBenefit.ndjson" = "fafd76d16c335de44d1b28c388ac3de873809d3b2c429612ec6b8741db0fa82f"
"bcda-site/v3_Patient.ndjson" = "7eb427266676415c6beee0fa705d7ad31d2335971f711bdfe0c63712392ba351"
"bcda-static-site/LICENSE" = "6d489af6292662d9e36d34ce49423784984a5f6e41d7b58f49b01264df59fa03"
"bcda-static-site/src/_announcements/2025-py-data-limit-ended.md" = "ff8014d6f2bbe232d176c62ea7999ad941fb584444242524b2e3615b2b8203d6"
"bcda-static-site/src/_announcements/bcda-data-limited-in-jan-2026-for-yearly-updates.md" = "bcf226627c84fe91c99f47c3987b426806ed458da474b7ce38e1a3029209410e"
"bcda-static-site/src/_announcements/bcda-launches-v3.md" = "efadb9f0a157844ff5b818273eb53fb5c719f12db35bc0f80384fffc3ab4f99c"
"bcda-static-site/src/_announcements/bcda-performance-improvement-to-speed-up-job-requests.md" = "9729ffa12daac14a6ae398fb841a0b0df414046cd5fb625ecf1eb862069313ba"
"bcda-static-site/src/_announcements/bcda-v3-expanded-access-general-availability.md" = "e0404747067c0c2723ec49b357eba68f5b3d8f934bf4c9221b1b3810cbbf44ff"
"bcda-static-site/src/_pages/api-documentation/access-claims-data.md" = "c48ef7cbc7a9fd54161758245d8de0ce3b81f030453da7ecb2f37ccc4eea6e57"
"bcda-static-site/src/_pages/api-documentation/filter-claims-data.md" = "e098ac1af2283796a1b64eb9b30c396a98f281e7a13de0f74a926f13c7412a69"
"bcda-static-site/src/_pages/api-documentation/how-to-migrate-v3.md" = "b07d928b182e41cfef65eb50a9d04d846cbca2120b8d4eab472b4d2df2689d76"
"bcda-static-site/src/_pages/bcda-data/comparison-bcda-cclf-files.md" = "c87000f64a7a16e1180d654881ce561ad7c63885f9228c8d78be10e703792a28"
"bcda-static-site/src/_pages/bcda-data/partially-adjudicated-claims-data.md" = "cc65fa79d657b6c41d5c989b830bf7e64adb62270cd7fbc0728123265d5423d8"
"bcda-static-site/src/_pages/support/faqs.md" = "f8655fb91ba744485a70adcf08491e04ee059ace7e7054769059b75cfbfed875"

View File

@@ -0,0 +1,603 @@
package client
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
"github.com/ccoveille/go-safecast"
backoff "github.com/cenkalti/backoff/v4"
fhirClient "github.com/CMSgov/bcda-app/bcda/client/fhir"
"github.com/CMSgov/bcda-app/bcda/constants"
"github.com/CMSgov/bcda-app/bcda/fhir"
"github.com/CMSgov/bcda-app/bcda/utils"
"github.com/CMSgov/bcda-app/bcdaworker/queueing/worker_types"
"github.com/CMSgov/bcda-app/conf"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/pborman/uuid"
)
var logger logrus.FieldLogger
const (
clientIDHeader = "BULK-CLIENTID"
jobIDHeader = "BULK-JOBID"
TransactionIDHeader = "TRANSACTIONID"
)
// BlueButtonConfig holds the configuration settings needed to create a BlueButtonClient
// TODO (BCDA-3755): Move the other env vars used in NewBlueButtonClient to this struct
type BlueButtonConfig struct {
BBServer string
BBBasePath string
}
// NewConfig generates a new BlueButtonConfig using various environment variables.
func NewConfig(basePath string) BlueButtonConfig {
var server string
if basePath == constants.BFDV3Path {
server = conf.GetEnv("V3_BB_SERVER_LOCATION")
} else {
server = conf.GetEnv("BB_SERVER_LOCATION")
}
return BlueButtonConfig{
BBServer: server,
BBBasePath: basePath,
}
}
type ClaimsWindow struct {
Earliest time.Time
Latest time.Time
}
type APIClient interface {
GetExplanationOfBenefit(jobData worker_types.JobEnqueueArgs, patientID string, claimsWindow ClaimsWindow) (*fhir.Bundle, error)
GetPatient(jobData worker_types.JobEnqueueArgs, patientID string) (*fhir.Bundle, error)
GetCoverage(jobData worker_types.JobEnqueueArgs, beneficiaryID string) (*fhir.Bundle, error)
GetPatientByMbi(jobData worker_types.JobEnqueueArgs, mbi string) (string, error)
GetClaim(jobData worker_types.JobEnqueueArgs, mbi string, claimsWindow ClaimsWindow) (*fhir.Bundle, error)
GetClaimResponse(jobData worker_types.JobEnqueueArgs, mbi string, claimsWindow ClaimsWindow) (*fhir.Bundle, error)
}
type BlueButtonClient struct {
client fhirClient.Client
maxTries uint64
retryInterval time.Duration
bbServer string
BBBasePath string
}
// Ensure BlueButtonClient satisfies the interface
var _ APIClient = &BlueButtonClient{}
func NewBlueButtonClient(config BlueButtonConfig) (*BlueButtonClient, error) {
certFile := conf.GetEnv("BB_CLIENT_CERT_FILE")
keyFile := conf.GetEnv("BB_CLIENT_KEY_FILE")
pageSize := utils.GetEnvInt("BB_CLIENT_PAGE_SIZE", 0)
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, errors.Wrap(err, "could not load Blue Button keypair")
}
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}
if strings.ToLower(conf.GetEnv("BB_CHECK_CERT")) != "false" {
caFilePaths := strings.Split(conf.GetEnv("BB_CLIENT_CA_FILE"), ",")
caCertPool := x509.NewCertPool()
for _, caFile := range caFilePaths {
caCert, err := os.ReadFile(filepath.Clean(caFile))
if err != nil {
return nil, errors.Wrap(err, "could not read CA file")
}
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
return nil, errors.New("could not append CA certificate(s)")
}
}
tlsConfig.RootCAs = caCertPool
} else {
tlsConfig.InsecureSkipVerify = true // #nosec G402 this is only used in localdev/ci testing
logger.Warn("Blue Button certificate check disabled")
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
// Ensure that we have compression enabled. This allows the transport to request for gzip content
// and handle the decompression transparently.
// See: https://golang.org/src/net/http/transport.go?s=3396:10950#L182 for more information
DisableCompression: false,
}
var timeout int
if timeout, err = strconv.Atoi(conf.GetEnv("BB_TIMEOUT_MS")); err != nil {
logger.Warn(errors.Wrap(err, "Could not get Blue Button timeout from environment variable; using default value of 10000."))
timeout = 10000
}
hl := &httpLogger{transport, logger}
httpClient := &http.Client{Transport: hl, Timeout: time.Duration(timeout) * time.Millisecond}
client := fhirClient.NewClient(httpClient, pageSize)
maxTries, err := safecast.ToUint64(utils.GetEnvInt("BB_REQUEST_MAX_TRIES", 3))
if err != nil {
logger.Warn(errors.Wrap(err, "Could not convert Blue Button max retries from environment variable"))
}
retryInterval := time.Duration(utils.GetEnvInt("BB_REQUEST_RETRY_INTERVAL_MS", 1000)) * time.Millisecond
return &BlueButtonClient{client, maxTries, retryInterval, config.BBServer, config.BBBasePath}, nil
}
// SetLogger sets the logger to be used in the client.
// Since both the API and worker use the bluebutton client, we need
// to be able to set the appropriate logger based on who is using the client.
func SetLogger(log logrus.FieldLogger) {
logger = log
}
func (bbc *BlueButtonClient) GetPatient(jobData worker_types.JobEnqueueArgs, patientID string) (*fhir.Bundle, error) {
header := make(http.Header)
header.Add("IncludeAddressFields", "true")
params := GetDefaultParams()
params.Set("_id", patientID)
updateParamWithLastUpdated(&params, jobData.Since, jobData.TransactionTime)
u, err := bbc.getURL("Patient", params)
if err != nil {
return nil, err
}
return bbc.makeBundleDataRequest("GET", u, jobData, header, nil)
}
func (bbc *BlueButtonClient) GetPatientByMbi(jobData worker_types.JobEnqueueArgs, mbi string) (string, error) {
headers := createURLEncodedHeader()
params := GetDefaultParams()
params.Set("identifier", fmt.Sprintf("http://hl7.org/fhir/sid/us-mbi|%s", mbi))
u, err := bbc.getURL("Patient/_search", url.Values{})
if err != nil {
return "", err
}
return bbc.getRawData("POST", jobData, u, headers, strings.NewReader(params.Encode()))
}
func (bbc *BlueButtonClient) GetCoverage(jobData worker_types.JobEnqueueArgs, beneficiaryID string) (*fhir.Bundle, error) {
params := GetDefaultParams()
params.Set("beneficiary", beneficiaryID)
updateParamWithLastUpdated(&params, jobData.Since, jobData.TransactionTime)
u, err := bbc.getURL("Coverage", params)
if err != nil {
return nil, err
}
return bbc.makeBundleDataRequest("GET", u, jobData, nil, nil)
}
func (bbc *BlueButtonClient) GetClaim(jobData worker_types.JobEnqueueArgs, mbi string, claimsWindow ClaimsWindow) (*fhir.Bundle, error) {
headers := createURLEncodedHeader()
params := GetDefaultParams()
updateParamsWithClaimsDefaults(&params, mbi)
updateParamWithServiceDate(&params, claimsWindow)
updateParamWithLastUpdated(&params, jobData.Since, jobData.TransactionTime)
u, err := bbc.getURL("Claim/_search", url.Values{})
if err != nil {
return nil, err
}
return bbc.makeBundleDataRequest("POST", u, jobData, headers, strings.NewReader(params.Encode()))
}
func (bbc *BlueButtonClient) GetClaimResponse(jobData worker_types.JobEnqueueArgs, mbi string, claimsWindow ClaimsWindow) (*fhir.Bundle, error) {
headers := createURLEncodedHeader()
params := GetDefaultParams()
updateParamsWithClaimsDefaults(&params, mbi)
updateParamWithServiceDate(&params, claimsWindow)
updateParamWithLastUpdated(&params, jobData.Since, jobData.TransactionTime)
u, err := bbc.getURL("ClaimResponse/_search", url.Values{})
if err != nil {
return nil, err
}
return bbc.makeBundleDataRequest("POST", u, jobData, headers, strings.NewReader(params.Encode()))
}
func (bbc *BlueButtonClient) GetExplanationOfBenefit(jobData worker_types.JobEnqueueArgs, patientID string, claimsWindow ClaimsWindow) (*fhir.Bundle, error) {
header := make(http.Header)
header.Add("IncludeTaxNumbers", "true")
params := GetDefaultParams()
params.Set("patient", patientID)
if bbc.BBBasePath != constants.BFDV3Path {
params.Set("excludeSAMHSA", "true")
} else {
params.Set("_security:not", "42CFRPart2")
}
updateParamWithServiceDate(&params, claimsWindow)
updateParamWithLastUpdated(&params, jobData.Since, jobData.TransactionTime)
updateParamWithTypeFilter(&params, jobData.TypeFilter)
setRestrictiveServiceDateWindow(&params)
u, err := bbc.getURL("ExplanationOfBenefit", params)
if err != nil {
return nil, err
}
return bbc.makeBundleDataRequest("GET", u, jobData, header, nil)
}
func (bbc *BlueButtonClient) GetMetadata() (string, error) {
u, err := bbc.getURL("metadata", GetDefaultParams())
if err != nil {
return "", err
}
jobData := worker_types.JobEnqueueArgs{}
return bbc.getRawData("GET", jobData, u, nil, nil)
}
func (bbc *BlueButtonClient) makeBundleDataRequest(method string, u *url.URL, jobData worker_types.JobEnqueueArgs, headers http.Header, body io.Reader) (*fhir.Bundle, error) {
var b *fhir.Bundle
for ok := true; ok; {
result, nextURL, err := bbc.tryBundleRequest(method, u, jobData, headers, body)
if err != nil {
return nil, err
}
if b == nil {
b = result
} else {
b.Entries = append(b.Entries, result.Entries...)
}
u = nextURL
ok = nextURL != nil
}
return b, nil
}
func (bbc *BlueButtonClient) tryBundleRequest(method string, u *url.URL, jobData worker_types.JobEnqueueArgs, headers http.Header, body io.Reader) (*fhir.Bundle, *url.URL, error) {
var (
result *fhir.Bundle
nextURL *url.URL
err error
)
eb := backoff.NewExponentialBackOff()
eb.InitialInterval = bbc.retryInterval
b := backoff.WithMaxRetries(eb, bbc.maxTries)
err = backoff.RetryNotify(func() error {
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
logger.Error(err)
return err
}
for key, values := range headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
addDefaultRequestHeaders(req, uuid.NewRandom(), jobData)
result, nextURL, err = bbc.client.DoBundleRequest(req)
if err != nil {
logger.Error(err)
}
return err
},
b,
func(err error, d time.Duration) {
logger.Infof("Blue Button request failed %s. Retry in %s", err.Error(), d.String())
},
)
if err != nil {
return nil, nil, fmt.Errorf("blue button request failed %d time(s) %s", bbc.maxTries, err.Error())
}
return result, nextURL, nil
}
func (bbc *BlueButtonClient) getRawData(method string, jobData worker_types.JobEnqueueArgs, u *url.URL, headers http.Header, body io.Reader) (string, error) {
eb := backoff.NewExponentialBackOff()
eb.InitialInterval = bbc.retryInterval
b := backoff.WithMaxRetries(eb, bbc.maxTries)
var result string
err := backoff.RetryNotify(func() error {
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
logger.Error(err)
return err
}
for key, values := range headers {
for _, value := range values {
req.Header.Add(key, value)
}
}
addDefaultRequestHeaders(req, uuid.NewRandom(), jobData)
result, err = bbc.client.DoRaw(req)
if err != nil {
logger.Error(err)
}
return err
},
b,
func(err error, d time.Duration) {
logger.Infof("Blue Button request failed %s. Retry in %s", err, d.String())
},
)
if err != nil {
return "", fmt.Errorf("blue button request failed %d time(s) %s", bbc.maxTries, err.Error())
}
return result, nil
}
func (bbc *BlueButtonClient) getURL(path string, params url.Values) (*url.URL, error) {
u, err := url.Parse(fmt.Sprintf("%s%s/%s/", bbc.bbServer, bbc.BBBasePath, path))
if err != nil {
return nil, err
}
u.RawQuery = params.Encode()
return u, nil
}
func addDefaultRequestHeaders(req *http.Request, reqID uuid.UUID, jobData worker_types.JobEnqueueArgs) {
// Info for BB backend: https://jira.cms.gov/browse/BLUEBUTTON-483
req.Header.Add("keep-alive", "")
req.Header.Add(constants.BBHeaderTS, time.Now().String())
req.Header.Add(constants.BBHeaderOriginQID, reqID.String())
req.Header.Add(constants.BBHeaderOriginQC, "1")
req.Header.Add(constants.BBHeaderOriginURL, req.URL.String())
req.Header.Add("IncludeIdentifiers", "mbi")
req.Header.Add(jobIDHeader, strconv.Itoa(jobData.ID))
req.Header.Add(clientIDHeader, jobData.CMSID)
req.Header.Add(TransactionIDHeader, jobData.TransactionID)
// We SHOULD NOT be specifying "Accept-Encoding: gzip" on the request header.
// If we specify this header at the client level, then we must be responsible for decompressing the response.
// This header should be automatically set by the underlying http.Transport which will handle the decompression transparently
// Details: https://golang.org/src/net/http/transport.go#L2432
// req.Header.Add("Accept-Encoding", "gzip")
// Do not set BB-specific headers with blank values
// Leaving them here, commented out, in case we want to set them to real values in future
//req.Header.Add("BlueButton-BeneficiaryId", "")
//req.Header.Add("BlueButton-OriginatingIpAddress", "")
//req.Header.Add("BlueButton-BackendCall", "")
}
func GetDefaultParams() (params url.Values) {
params = url.Values{}
params.Set("_format", "application/fhir+json")
return params
}
// updateParamWithServiceDate adds service-date query parameters for the ClaimsWindow,
// requesting claims on or after Earliest ("ge") and on or before Latest ("le").
func updateParamWithServiceDate(params *url.Values, claimsWindow ClaimsWindow) {
// ServiceDate only uses yyyy-mm-dd
const isoDate = "2006-01-02"
if !claimsWindow.Earliest.IsZero() {
params.Add("service-date", fmt.Sprintf("ge%s", claimsWindow.Earliest.Format(isoDate)))
}
if !claimsWindow.Latest.IsZero() {
params.Add("service-date", fmt.Sprintf("le%s", claimsWindow.Latest.Format(isoDate)))
}
}
type serviceDateVal struct {
prefix string
date time.Time
}
// setRestrictiveServiceDateWindow sets the most restrictive window of time from which to pull data from BFD via service-date params.
// BFD only allows for one earliest boundary and one latest boundary.
// We need to do some comparisons to make sure we apply the most restrictive option for each.
// eg "gt2025" is a more restrictive earliest boundary than "gt2024" as that will only get us data starting from 2025 and not all the way back to 2024.
// known edge cases that are currently not accounted for (low risk, low priority, time constraints):
// - we dont allow for hours, mins, secs, tz, etc
func setRestrictiveServiceDateWindow(params *url.Values) {
serviceDates := (*params)["service-date"]
if len(serviceDates) <= 0 {
return
}
var earliestDates, latestDates []serviceDateVal
for _, date := range serviceDates {
if strings.HasPrefix(date, "ge") || strings.HasPrefix(date, "gt") {
parsed, err := fhir.ParseDateString(date[2:])
if err == nil {
earliestDates = append(earliestDates, serviceDateVal{prefix: date[:2], date: parsed})
}
} else if strings.HasPrefix(date, "le") || strings.HasPrefix(date, "lt") {
parsed, err := fhir.ParseDateString(date[2:])
if err == nil {
latestDates = append(latestDates, serviceDateVal{prefix: date[:2], date: parsed})
}
} else if strings.HasPrefix(date, "eq") {
parsed, err := fhir.ParseDateString(date[2:])
if err == nil {
earliestDates = append(earliestDates, serviceDateVal{prefix: "ge", date: parsed})
latestDates = append(latestDates, serviceDateVal{prefix: "lt", date: parseLatestDateFromEqualPrefix(date[2:])})
}
} else if strings.HasPrefix(date, "20") {
parsed, err := fhir.ParseDateString(date)
if err == nil {
earliestDates = append(earliestDates, serviceDateVal{prefix: "ge", date: parsed})
latestDates = append(latestDates, serviceDateVal{prefix: "lt", date: parseLatestDateFromEqualPrefix(date)})
}
}
}
// remove any zero dates to cleanse list of invalids
earliestDates = slices.DeleteFunc(earliestDates, func(s serviceDateVal) bool { return s.date.IsZero() })
latestDates = slices.DeleteFunc(latestDates, func(s serviceDateVal) bool { return s.date.IsZero() })
// sort earliest in descending order, and latest in ascending order so we can pick the most restrictive for each
slices.SortFunc(earliestDates, func(a, b serviceDateVal) int {
if c := b.date.Compare(a.date); c != 0 {
return c
}
if a.prefix == "gt" {
return -1
}
if b.prefix == "gt" {
return 1
}
return 0
})
slices.SortFunc(latestDates, func(a, b serviceDateVal) int {
if c := a.date.Compare(b.date); c != 0 {
return c
}
if a.prefix == "lt" {
return -1
}
if b.prefix == "lt" {
return 1
}
return 0
})
// remove all existing service-date instances so we can start from scratch
params.Del("service-date")
if len(earliestDates) > 0 {
params.Add("service-date", earliestDates[0].prefix+earliestDates[0].date.Format("2006-01-02"))
}
if len(latestDates) > 0 {
params.Add("service-date", latestDates[0].prefix+latestDates[0].date.Format("2006-01-02"))
}
}
func parseLatestDateFromEqualPrefix(date string) time.Time {
// date interval of a day; latest date is the beginning of the next day
parsedDate, err := time.Parse("2006-01-02", date)
if err == nil {
return parsedDate.AddDate(0, 0, 1)
}
// date interval of a month; latest date is the beginning of first day of the next month
parsedDate, err = time.Parse("2006-01", date)
if err == nil {
return parsedDate.AddDate(0, 1, 0)
}
// date interval of a year; latest date is the beginning of the first day of the next year
parsedDate, err = time.Parse("2006", date)
if err == nil {
return parsedDate.AddDate(1, 0, 0)
}
// if a time is specified, treat it like a day interval; latest date is the beginning of the next day
// ToDo: update this and upstream/downstream logic to properly handle times
parsedDate, err = fhir.ParseDateString(date)
if err == nil {
return parsedDate.AddDate(0, 0, 1)
}
return time.Time{}
}
func updateParamWithLastUpdated(params *url.Values, since string, transactionTime time.Time) {
// upper bound will always be set
params.Set("_lastUpdated", "le"+transactionTime.Format(time.RFC3339Nano))
// only set the lower bound parameter if it exists and begins with "gt" (to align with what is expected in _lastUpdated)
if len(since) > 0 && strings.HasPrefix(since, "gt") {
params.Add("_lastUpdated", since)
}
}
func updateParamWithTypeFilter(params *url.Values, typeFilter fhir.TypeFilterSubquery) {
for _, subqueryParam := range typeFilter.QueryParameters {
params.Add(subqueryParam.Name, subqueryParam.Value)
}
}
func updateParamsWithClaimsDefaults(params *url.Values, mbi string) {
params.Set("excludeSAMHSA", "true")
params.Set("includeTaxNumbers", "true")
params.Set("isHashed", "false")
params.Set("mbi", mbi)
}
func createURLEncodedHeader() http.Header {
headers := make(http.Header)
headers.Add("Content-Type", "application/x-www-form-urlencoded")
return headers
}
type httpLogger struct {
t *http.Transport
l logrus.FieldLogger
}
func (h *httpLogger) RoundTrip(req *http.Request) (*http.Response, error) {
go h.logRequest(req.Clone(context.Background()))
resp, err := h.t.RoundTrip(req)
if resp != nil {
h.logResponse(req, resp)
}
return resp, err
}
func (h *httpLogger) logRequest(req *http.Request) {
h.l.WithFields(logrus.Fields{
"bb_query_id": req.Header.Get(constants.BBHeaderOriginQID),
"bb_query_ts": req.Header.Get(constants.BBHeaderTS),
"bb_uri": req.URL.String(),
"job_id": req.Header.Get(jobIDHeader),
"cms_id": req.Header.Get(clientIDHeader),
}).Infoln("request")
}
func (h *httpLogger) logResponse(req *http.Request, resp *http.Response) {
h.l.WithFields(logrus.Fields{
"resp_status": resp.StatusCode,
"bb_query_id": req.Header.Get(constants.BBHeaderOriginQID),
"bb_query_ts": req.Header.Get(constants.BBHeaderTS),
"bb_uri": req.URL.String(),
"job_id": req.Header.Get(jobIDHeader),
"cms_id": req.Header.Get(clientIDHeader),
"transaction_id": req.Header.Get(TransactionIDHeader),
}).Infoln("response")
}

View File

@@ -0,0 +1,68 @@
package constants
const DevACOUUID = "0c527d2e-2e8a-4808-b11d-0fa06baf8254"
const SmallACOUUID = "3461C774-B48F-11E8-96F8-529269fb1459"
const MediumACOUUID = "C74C008D-42F8-4ED9-BF88-CEE659C7F692"
const LargeACOUUID = "8D80925A-027E-43DD-8AED-9A501CC4CD91"
const ImportInprog = "In-Progress"
const ImportComplete = "Completed"
const ImportFail = "Failed"
// This is set during compilation. See dockerfiles for usage
var Version = "latest"
const Adjudicated = "adjudicated"
const PartiallyAdjudicated = "partially-adjudicated"
// Cli.go constants
const CliCMSIDArg = "cms-id"
const CliCMSIDDesc = "CMS ID of ACO"
const CliArchDesc = "How long files should wait in archive before deletion"
const CliRemoveArchDesc = "Remove job directory and files from archive and update job status to Expired"
const CliAuthToolsCategory = "Authentication tools"
const CliDataImpCategory = "Data import"
const ContentType = "Content-Type"
const JsonContentType = "application/json"
const FHIRJsonContentType = "application/fhir+json"
const BBHeaderTS = "BlueButton-OriginalQueryTimestamp"
const BBHeaderOriginURL = "BlueButton-OriginalUrl"
const BBHeaderOriginQID = "BlueButton-OriginalQueryId"
const BBHeaderOriginQ = "BlueButton-OriginalQuery"
const BBHeaderOriginQC = "BlueButton-OriginalQueryCounter"
const CCLFFileRetID = "%s RETURNING id"
const JobKeyCreateErr = "failed to create job key: %w"
const JOBIDPath = "/jobs/{jobID}"
const IssuerSSAS = "ssas"
const EmptyString = ""
const FiveSeconds = "5"
const FiveHundredSeconds = "500"
const CCLF8FileNum = int(8)
const BFDV1Path = "/v1/fhir"
const BFDV2Path = "/v2/fhir"
const BFDV3Path = "/v3/fhir"
const V3Version = "v3"
const GetExistingBenes = "GetExistingBenes"
const GetNewAndExistingBenes = "GetNewAndExistingBenes"
const DefaultRegion = "us-east-1"
const PublisherName = "Centers for Medicare & Medicaid Services"
const SoftwareName = "Beneficiary Claims Data API"
const SoftwareDescription = "The Beneficiary Claims Data API (BCDA) enables healthcare organizations participating in the Medicare Shared Savings Program or a CMS Innovation Model to retrieve Medicare Part A, Part B, and Part D claims data for their prospectively assigned or assignable beneficiaries."
const SmartOAuthURIsExtensionURL = "http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"
const RestfulSecurityServiceSystem = "http://terminology.hl7.org/CodeSystem/restful-security-service"
const BFDSystemTypeURL = "https://bluebutton.cms.gov/fhir/CodeSystem/System-Type"
const BFDFinalActionURL = "https://bluebutton.cms.gov/fhir/CodeSystem/Final-Action"
const WarningsAndInfoFileName = "warnings-and-info.ndjson"

View File

@@ -0,0 +1,9 @@
package constants
type DataRequestType uint8
const (
DefaultRequest DataRequestType = iota
RetrieveNewBeneHistData // Allows caller to retrieve all of the data for newly attributed beneficiaries
Runout // Allows caller to retrieve claims data for beneficiaries no longer attributed to the ACO
)

304
dev/vendor/bcda-app/bcda/fhir/param.go vendored Normal file
View File

@@ -0,0 +1,304 @@
package fhir
import (
"fmt"
"slices"
"strings"
"time"
"github.com/CMSgov/bcda-app/bcda/constants"
)
type TypeFilterSubquery struct {
ResourceType string
QueryParameters []TypeFilterSubqueryParam
}
type TypeFilterSubqueryParam struct {
Name string
Value string
}
type TypeFilterParamName string
const (
TypeFilterParamOutcome TypeFilterParamName = "outcome"
TypeFilterParamServiceDate TypeFilterParamName = "service-date"
TypeFilterParamTag TypeFilterParamName = "_tag"
)
type ParameterParsingError struct {
Details string
}
func (e ParameterParsingError) Error() string {
return fmt.Sprintf("malformed parameter: %s", e.Details)
}
type ParameterValidationError struct {
Details string
}
func (e ParameterValidationError) Error() string {
return fmt.Sprintf("invalid parameter: %s", e.Details)
}
func ParseTypeFilterSubquery(s string) (TypeFilterSubquery, error) {
// Expected format is: <resourceType>?<paramList>
resourceType, params, found := strings.Cut(s, "?")
if !found {
return TypeFilterSubquery{}, ParameterParsingError{Details: fmt.Sprintf("_typeFilter parameter missing question mark %s", s)}
} else if len(resourceType) == 0 {
return TypeFilterSubquery{}, ParameterParsingError{Details: fmt.Sprintf("_typeFilter parameter missing resource type %s", s)}
} else if len(params) == 0 {
return TypeFilterSubquery{}, ParameterParsingError{Details: fmt.Sprintf("_typeFilter parameter missing value %s", s)}
}
var subqueryParams []TypeFilterSubqueryParam
paramAry := strings.SplitSeq(params, "&")
for paramPair := range paramAry {
name, value, found := strings.Cut(paramPair, "=")
if !found {
return TypeFilterSubquery{}, ParameterParsingError{Details: fmt.Sprintf("_typeFilter value missing equals sign: %s", paramPair)}
}
subqueryParams = append(subqueryParams, TypeFilterSubqueryParam{Name: name, Value: value})
}
return TypeFilterSubquery{ResourceType: resourceType, QueryParameters: subqueryParams}, nil
}
func ValidateTypeFilterSubquery(subquery TypeFilterSubquery) error {
if subquery.ResourceType != "ExplanationOfBenefit" {
return ParameterValidationError{Details: fmt.Sprintf("invalid _typeFilter Resource Type (Only EOBs valid): %s", subquery.ResourceType)}
}
var serviceDateParams []DateParam
for _, param := range subquery.QueryParameters {
if strings.HasPrefix(param.Name, string(TypeFilterParamTag)) {
tag, err := ParseTokenParam(param)
if err != nil {
return err
}
err = ValidateTag(tag)
if err != nil {
return err
}
} else if strings.HasPrefix(param.Name, string(TypeFilterParamServiceDate)) {
date, err := ParseDateParam(param)
if err != nil {
return err
}
serviceDateParams = append(serviceDateParams, date)
} else if strings.HasPrefix(param.Name, string(TypeFilterParamOutcome)) {
outcome, err := ParseTokenParam(param)
if err != nil {
return err
}
err = ValidateOutcome(outcome)
if err != nil {
return err
}
} else {
return ParameterValidationError{Details: fmt.Sprintf("invalid _typeFilter subquery parameter: %s", param.Name)}
}
}
err := ValidateServiceDates(serviceDateParams)
if err != nil {
return err
}
return nil
}
func GetTagParams(subquery TypeFilterSubquery) ([]TokenParam, error) {
tagParams := []TokenParam{}
for _, param := range subquery.QueryParameters {
if strings.HasPrefix(param.Name, string(TypeFilterParamTag)) {
tag, err := ParseTokenParam(param)
if err != nil {
return nil, err
}
tagParams = append(tagParams, tag)
}
}
return tagParams, nil
}
// https://hl7.org/fhir/R4/search.html#string
type StringParam struct {
Name string
Modifier string
Values []string
}
func ParseStringParam(subqueryParam TypeFilterSubqueryParam) (StringParam, error) {
s := StringParam{}
s.Name, s.Modifier, _ = strings.Cut(subqueryParam.Name, ":")
if len(s.Name) == 0 {
return s, ParameterParsingError{Details: "string parameter missing name"}
}
if len(subqueryParam.Value) == 0 {
return s, ParameterParsingError{Details: fmt.Sprintf("string parameter missing value: %s", s.Name)}
}
s.Values = strings.Split(subqueryParam.Value, ",")
return s, nil
}
// https://hl7.org/fhir/R4/search.html#date
type DateParam struct {
Name string
Prefix string
Datetimes []string
}
func ParseDateParam(subqueryParam TypeFilterSubqueryParam) (DateParam, error) {
d := DateParam{}
if len(subqueryParam.Name) == 0 {
return d, ParameterParsingError{Details: "date parameter missing key"}
}
d.Name = subqueryParam.Name
if len(subqueryParam.Value) == 0 {
return d, ParameterParsingError{Details: fmt.Sprintf("date parameter missing value: %s", d.Name)}
}
// BFD only supports eq, ge, gt, lt, le as of 2026-08-17. See: https://cmsgov.slack.com/archives/CMT1YS2KY/p1786716165942379
var prefixes = []string{"eq", "lt", "gt", "le", "ge"} // "ne", "sa", "eb", "ap"}
afterPrefix := ""
if len(subqueryParam.Value) >= 2 && slices.Contains(prefixes, subqueryParam.Value[:2]) {
d.Prefix = subqueryParam.Value[:2]
afterPrefix = subqueryParam.Value[2:]
} else {
afterPrefix = subqueryParam.Value
}
if len(afterPrefix) == 0 {
return d, ParameterParsingError{Details: fmt.Sprintf("value must include a valid FHIR date for date parameter %s", d.Name)}
}
d.Datetimes = strings.Split(afterPrefix, ",")
for _, datetime := range d.Datetimes {
_, err := ParseDateString(datetime)
if err != nil {
return d, ParameterParsingError{Details: fmt.Sprintf("invalid date parameter value: %s", datetime)}
}
}
return d, nil
}
func ParseDateString(datetime string) (time.Time, error) {
var datetimeFormats = []string{
"2006",
"2006-01",
"2006-01-02",
"2006-01-02T15:04:05",
"2006-01-02T15:04:05+07:00",
"2006-01-02T15:04:05-07:00",
"2006-01-02T15:04:05Z",
}
for _, format := range datetimeFormats {
if parsed, err := time.Parse(format, datetime); err == nil {
return parsed, nil
}
}
return time.Time{}, ParameterParsingError{Details: fmt.Sprintf("invalid date parameter value: %s", datetime)}
}
// ValidateServiceDates ensures that service-date parameters have valid keys, non-delimited values,
// and at most one earliest ("gt"/"ge") and one latest ("lt"/"le") condition.
// Exact date conditions ("eq" or "") establish both an earliest and a latest boundary.
func ValidateServiceDates(serviceDateParams []DateParam) error {
earliestBoundCount, latestBoundCount := 0, 0
for _, sd := range serviceDateParams {
if sd.Name != string(TypeFilterParamServiceDate) {
return ParameterValidationError{Details: fmt.Sprintf("invalid key for service-date parameter: %s", sd.Name)}
}
if len(sd.Datetimes) > 1 {
return ParameterValidationError{Details: "invalid service-date parameter value: comma-separated values are not supported"}
}
switch sd.Prefix {
case "lt", "le":
latestBoundCount = latestBoundCount + 1
case "gt", "ge":
earliestBoundCount = earliestBoundCount + 1
case "eq", "":
latestBoundCount = latestBoundCount + 1
earliestBoundCount = earliestBoundCount + 1
}
}
if earliestBoundCount > 1 || latestBoundCount > 1 {
return ParameterValidationError{Details: "invalid service-date parameter value: conflicting prefix conditions"}
}
return nil
}
// https://hl7.org/fhir/R4/search.html#token
type TokenParam struct {
Name string
Modifier string
Values []TokenValue
}
type TokenValue struct {
System string
Code string
}
func ParseTokenParam(subqueryParam TypeFilterSubqueryParam) (TokenParam, error) {
t := TokenParam{}
if len(subqueryParam.Name) == 0 {
return t, ParameterParsingError{Details: "token parameter missing key"}
}
t.Name, t.Modifier, _ = strings.Cut(subqueryParam.Name, ":")
if len(subqueryParam.Value) == 0 {
return t, ParameterParsingError{Details: fmt.Sprintf("token parameter missing value %s", t.Name)}
}
values := strings.SplitSeq(subqueryParam.Value, ",")
for value := range values {
before, after, found := strings.Cut(value, "|")
if found {
t.Values = append(t.Values, TokenValue{System: before, Code: after})
} else {
t.Values = append(t.Values, TokenValue{Code: before})
}
}
return t, nil
}
var validTagTokens = map[string][]string{
constants.BFDSystemTypeURL: {"SharedSystem", "NationalClaimsHistory", "DDPS"},
constants.BFDFinalActionURL: {"FinalAction", "NotFinalAction"},
}
func ValidateTag(t TokenParam) error {
if t.Name != string(TypeFilterParamTag) {
return ParameterValidationError{Details: fmt.Sprintf("invalid key for _tag parameter: %s", t.Name)}
}
if len(t.Modifier) > 0 {
return ParameterValidationError{Details: fmt.Sprintf("invalid _tag parameter; modifier %s not supported", t.Modifier)}
}
for _, value := range t.Values {
if len(value.System) == 0 || len(value.Code) == 0 {
return ParameterValidationError{Details: "invalid _tag parameter value. Searching by _tag requires a token (system|code) to be specified"}
}
validTagCodes, ok := validTagTokens[value.System]
if !ok || !slices.Contains(validTagCodes, value.Code) {
return ParameterValidationError{Details: fmt.Sprintf("invalid _tag parameter, unsupported code %s for system %s", value.Code, value.System)}
}
}
return nil
}
func ValidateOutcome(t TokenParam) error {
if t.Name != string(TypeFilterParamOutcome) {
return ParameterValidationError{Details: fmt.Sprintf("invalid key for outcome parameter: %s", t.Name)}
}
if len(t.Modifier) > 0 {
return ParameterValidationError{Details: fmt.Sprintf("invalid outcome parameter; modifier %s not supported", t.Modifier)}
}
for _, value := range t.Values {
if value.Code != "complete" && value.Code != "partial" {
return ParameterValidationError{Details: fmt.Sprintf("invalid outcome code: %s. Supported outcome codes are 'complete' and 'partial'", value.Code)}
}
}
return nil
}

View File

@@ -0,0 +1,174 @@
package models
import (
"fmt"
"strings"
"time"
"github.com/pborman/uuid"
)
const (
JobStatusArchived JobStatus = "Archived"
JobStatusCancelled JobStatus = "Cancelled"
JobStatusCancelledExpired JobStatus = "CancelledExpired" // JobStatusCancelledExpired represents a job that has been cancelled whose data has been cleaned up
JobStatusCompleted JobStatus = "Completed"
JobStatusExpired JobStatus = "Expired"
JobStatusFailed JobStatus = "Failed"
JobStatusFailedExpired JobStatus = "FailedExpired" // JobStatusFailedExpired represents a job that failed whose data has been cleaned up
JobStatusInProgress JobStatus = "In Progress"
JobStatusPending JobStatus = "Pending"
)
var AllJobStatuses = []JobStatus{
JobStatusArchived,
JobStatusCancelled,
JobStatusCancelledExpired,
JobStatusCompleted,
JobStatusExpired,
JobStatusFailed,
JobStatusFailedExpired,
JobStatusInProgress,
JobStatusPending,
}
type JobStatus string
type Job struct {
ID uint
ACOID uuid.UUID `json:"aco_id"`
RequestURL string `json:"request_url"` // request_url
Status JobStatus `json:"status"` // status
TransactionTime time.Time // most recent data load transaction time from BFD
JobCount int
CreatedAt time.Time
UpdatedAt time.Time
BenesAttributedToACO int // Total beneficiaries attributed to ACO at time of job request
}
func (j *Job) StatusMessage(numCompletedJobKeys int) string {
if j.Status == JobStatusInProgress && j.JobCount > 0 {
pct := float64(numCompletedJobKeys) / float64(j.JobCount) * 100
return fmt.Sprintf("%s (%d%%)", j.Status, int(pct))
}
return string(j.Status)
}
// BlankFileName contains the naming convention for empty ndjson file
const BlankFileName string = "blank.ndjson"
type JobKey struct {
ID uint
JobID uint `json:"job_id"`
// Although que_job records are temporary, we store the ID to ensure
// that workers are never duplicating job keys.
QueJobID *int64
FileName string
ResourceType string
BenesWithData int // count of beneficiaries with entry data
BenesRetrievedPercent int // percent of beneficiaries successfully retrieved from BFD
}
func (j *JobKey) IsError() bool {
return strings.Contains(j.FileName, "-error.ndjson")
}
// ACO represents an Accountable Care Organization.
type ACO struct {
ID uint
UUID uuid.UUID `json:"uuid"`
CMSID *string `json:"cms_id"`
Name string `json:"name"`
ClientID string `json:"client_id"`
GroupID string `json:"group_id"`
SystemID string `json:"system_id"`
TerminationDetails *Termination `json:"termination"`
}
// Denylisted returns bool based on TerminationDetails.
// If CutoffDate is specified, the ACO is denylisted when time.Now() >= CutoffDate.
// If CutoffDate is not specified, it falls back to checking DenylistType.
func (aco *ACO) Denylisted() bool {
if aco.TerminationDetails != nil {
if !aco.TerminationDetails.CutoffDate.IsZero() {
return !time.Now().Before(aco.TerminationDetails.CutoffDate)
}
return aco.TerminationDetails.DenylistType == Involuntary || aco.TerminationDetails.DenylistType == Voluntary
}
return false
}
type CCLFFileType int16
const (
FileTypeDefault CCLFFileType = iota
FileTypeRunout
)
// String returns the letter associated with the CCLFFileType.
func (t CCLFFileType) String() string {
return [...]string{"Y", "R"}[t]
}
type CCLFFile struct {
ID uint
CCLFNum int
Name string
ACOCMSID string
Timestamp time.Time
PerformanceYear int
ImportStatus string
Type CCLFFileType
// CreatedAt is automatically set by the database. This is only
// set/pulled when querying data in the DB.
CreatedAt time.Time
}
// "The MBI has 11 characters, like the Health Insurance Claim Number (HICN), which can have up to 11."
// https://www.cms.gov/Medicare/New-Medicare-Card/Understanding-the-MBI-with-Format.pdf
type CCLFBeneficiary struct {
ID uint
FileID uint
MBI string
BlueButtonID string
}
// A BenePrefsFile is a basic file representation that can be stored in a database.
type BenePrefsFile struct {
ID uint
Name string
Timestamp time.Time
ImportStatus string
}
// BenePrefsFilenameMetadata is metadata information parsed from the filename.
type BenePrefsFilenameMetadata struct {
Name string
Timestamp time.Time
FilePath string
Imported bool
DeliveryDate time.Time
FileID uint
}
func (m BenePrefsFilenameMetadata) String() string {
if m.FilePath != "" {
return m.FilePath
}
return m.Name
}
// A BenePrefsRecord represents a single record parsed from an bene-prefs file.
type BenePrefsRecord struct {
ID uint
FileID uint
MBI string
SourceCode string
EffectiveDt time.Time
PrefIndicator string
SAMHSASourceCode string
SAMHSAEffectiveDt time.Time
SAMHSAPrefIndicator string
ACOCMSID string
BeneficiaryLinkKey int
}

View File

@@ -0,0 +1,710 @@
package service
import (
"context"
goerrors "errors"
"fmt"
"regexp"
"slices"
"time"
"github.com/ccoveille/go-safecast"
"github.com/pborman/uuid"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/CMSgov/bcda-app/bcda/constants"
bcdaerrors "github.com/CMSgov/bcda-app/bcda/errors"
"github.com/CMSgov/bcda-app/bcda/models"
"github.com/CMSgov/bcda-app/bcda/utils"
"github.com/CMSgov/bcda-app/bcdaworker/queueing/worker_types"
"github.com/CMSgov/bcda-app/conf"
"github.com/CMSgov/bcda-app/log"
"github.com/CMSgov/bcda-app/middleware"
)
// Ensure service satisfies the interface
var _ Service = &service{}
// Service contains all of the methods needed to interact with the data represented in the models package
type Service interface {
GetCutoffTime(ctx context.Context, reqType constants.DataRequestType, since time.Time, timeConstraints TimeConstraints, fileType models.CCLFFileType) (time.Time, string)
FindOldCCLFFile(ctx context.Context, cmsID string, since time.Time, cclfTimestamp time.Time) (uint, error)
GetQueJobs(ctx context.Context, args worker_types.PrepareJobArgs) (queJobs []*worker_types.JobEnqueueArgs, benesAttributed int, err error)
GetJobAndKeys(ctx context.Context, jobID uint) (*models.Job, []*models.JobKey, error)
GetJobKey(ctx context.Context, jobID uint, filename string) (*models.JobKey, error)
GetJobs(ctx context.Context, acoID uuid.UUID, statuses ...models.JobStatus) ([]*models.Job, error)
CancelJob(ctx context.Context, jobID uint) (uint, error)
GetJobPriority(acoID string, resourceType string, sinceParam bool) int16
GetLatestCCLFFile(ctx context.Context, cmsID string, earlierTime time.Time, laterTime time.Time, fileType models.CCLFFileType) (*models.CCLFFile, error)
GetACOConfigForID(cmsID string) (*ACOConfig, bool)
GetTimeConstraints(ctx context.Context, cmsID string) (TimeConstraints, error)
GetAttributionStatusDates(ctx context.Context, cmsID string, timeConstraints TimeConstraints, fileType models.CCLFFileType) (lastUpdated time.Time, expirationDate time.Time, err error)
IsV3NoPartialClaimsModel(model string) bool
}
type service struct {
repository models.Repository
logger logrus.FieldLogger
stdCutoffDuration time.Duration
sp suppressionParameters
rp runoutParameters
bbBasePath string
// These are always searched in order and first matching config is used for any given ACO.
acoConfigs []ACOConfig
v3NoPartialClaimsModels []string
}
func NewService(r models.Repository, cfg *Config, basePath string) Service {
return &service{
repository: r,
logger: log.API,
stdCutoffDuration: cfg.CutoffDuration,
sp: suppressionParameters{
includeSuppressedBeneficiaries: false,
lookbackDays: cfg.SuppressionLookbackDays,
},
rp: runoutParameters{
// Runouts apply to claims data for the previous year.
claimThruDate: cfg.RunoutConfig.claimThru,
CutoffDuration: cfg.RunoutConfig.CutoffDuration,
},
bbBasePath: basePath,
acoConfigs: cfg.ACOConfigs,
v3NoPartialClaimsModels: cfg.V3NoPartialClaimsModels,
}
}
type TimeConstraints struct {
AttributionDate time.Time
OptOutDate time.Time
ClaimsDate time.Time
}
type suppressionParameters struct {
includeSuppressedBeneficiaries bool
lookbackDays int
}
type runoutParameters struct {
// All claims data occur at or before this date
claimThruDate time.Time
// Amount of time the callers can retrieve runout data (relative to when runout data was ingested)
CutoffDuration time.Duration
}
func (s *service) GetCutoffTime(ctx context.Context, reqType constants.DataRequestType, since time.Time, timeConstraints TimeConstraints, fileType models.CCLFFileType) (cutoffTime time.Time, complexDataRequestType string) {
hasAttributionDate := !timeConstraints.AttributionDate.IsZero()
// for default requests, runouts, or any requests where the Since parameter is
// after a terminated ACO's attribution date, we should only retrieve exisiting benes
if reqType == constants.DefaultRequest ||
reqType == constants.Runout ||
(hasAttributionDate && since.After(timeConstraints.AttributionDate)) {
complexDataRequestType = constants.GetExistingBenes
// only set a cutoffTime if there is no attributionDate set
if timeConstraints.AttributionDate.IsZero() {
if fileType == models.FileTypeDefault && s.stdCutoffDuration > 0 {
cutoffTime = time.Now().Add(-1 * s.stdCutoffDuration)
} else if fileType == models.FileTypeRunout && s.rp.CutoffDuration > 0 {
cutoffTime = time.Now().Add(-1 * s.rp.CutoffDuration)
}
}
} else if reqType == constants.RetrieveNewBeneHistData {
complexDataRequestType = constants.GetNewAndExistingBenes
// only set cutoffTime if there is no attributionDate set
if s.stdCutoffDuration > 0 && timeConstraints.AttributionDate.IsZero() {
cutoffTime = time.Now().Add(-1 * s.stdCutoffDuration)
}
}
return cutoffTime, complexDataRequestType
}
// GetAttributionStatusDates provides the last updated date and the final date for which the most recent CCLF file for a CMS ID
// can be used to provide attribution information for data requests of a given file type.
//
// The expiration date can be limited by the date of the most recent CCLF file or the termination of the model entity in question.
func (s *service) GetAttributionStatusDates(ctx context.Context, cmsID string, timeConstraints TimeConstraints, fileType models.CCLFFileType) (lastUpdated time.Time, expirationDate time.Time, err error) {
latestFile, err := s.GetLatestCCLFFile(ctx, cmsID, expirationDate, expirationDate, fileType)
if err != nil {
s.logger.Errorf("could not fetch latest CCLF file for attribution status: %s", err)
return time.Time{}, time.Time{}, err
}
lastUpdated = latestFile.Timestamp
switch fileType {
case models.FileTypeDefault:
expirationDate = latestFile.Timestamp.Add(s.stdCutoffDuration)
case models.FileTypeRunout:
expirationDate = latestFile.Timestamp.Add(s.rp.CutoffDuration)
default:
s.logger.Errorf("unsupported file type for fetching attribution expiration date")
return lastUpdated, expirationDate, fmt.Errorf("unsupported file type for fetching attribution expiration date")
}
if !timeConstraints.AttributionDate.IsZero() && timeConstraints.AttributionDate.Before(expirationDate) {
return lastUpdated, timeConstraints.AttributionDate, nil
}
return lastUpdated, expirationDate, nil
}
// FindOldCCLFFile finds an older CCLF file depending on passed in timestamps
func (s *service) FindOldCCLFFile(ctx context.Context, cmsID string, since time.Time, cclfTimestamp time.Time) (cclfFileOldID uint, err error) {
// Retrieve an older CCLF file for beneficiary comparison.
// This should be older than cclfFileNew AND prior to the "since" parameter, if provided.
//
// e.g.
// - If it’s October 2023
// - and they request all beneficiary data “since January 1st, 2023"
// - any beneficiaries added in 2023 are considered "new."
//
oldFileTime := since
// If the _since parameter is more recent than the latest CCLF file timestamp, e
// set the old file's time to be older than the newest file's timestamp.
if !since.IsZero() && cclfTimestamp.Sub(since) < 0 {
oldFileTime = cclfTimestamp.Add(-1 * time.Second)
}
cclfFileOld, err := s.GetLatestCCLFFile(
ctx,
cmsID,
time.Time{},
oldFileTime,
models.FileTypeDefault,
)
if err != nil {
return 0, err
}
return cclfFileOld.ID, nil
}
func (s *service) GetQueJobs(ctx context.Context, args worker_types.PrepareJobArgs) (queJobs []*worker_types.JobEnqueueArgs, benesAttributed int, err error) {
var (
beneficiaries, newBeneficiaries []*models.CCLFBeneficiary
jobs []*worker_types.JobEnqueueArgs
)
// for default requests, runouts, or any requests where the Since parameter is
// after a terminated ACO's attribution date, we should only retrieve exisiting benes
switch args.ComplexDataRequestType {
case constants.GetExistingBenes:
beneficiaries, err = s.getBeneficiaries(ctx, args)
if err != nil {
return nil, 0, err
}
case constants.GetNewAndExistingBenes:
newBeneficiaries, beneficiaries, err = s.getNewAndExistingBeneficiaries(ctx, args)
if err != nil {
return nil, 0, err
}
// add new beneficiaries to the job queue; use a default time value to ensure
// that we retrieve the full history for these beneficiaries
jobs, err = s.createQueueJobs(ctx, args, time.Time{}, newBeneficiaries)
if err != nil {
return nil, 0, err
}
queJobs = append(queJobs, jobs...)
default:
return nil, 0, fmt.Errorf("unsupported RequestType %d", args.RequestType)
}
// add existiing beneficiaries to the job queue
jobs, err = s.createQueueJobs(ctx, args, args.Since, beneficiaries)
if err != nil {
return nil, 0, err
}
benesAttributed = len(newBeneficiaries) + len(beneficiaries)
args.Job.BenesAttributedToACO = benesAttributed
err = s.repository.UpdateJob(ctx, args.Job)
if err != nil {
return nil, 0, fmt.Errorf("failed to update job with benesAttributed: %w", err)
}
queJobs = append(queJobs, jobs...)
return queJobs, benesAttributed, nil
}
func (s *service) GetJobAndKeys(ctx context.Context, jobID uint) (*models.Job, []*models.JobKey, error) {
j, err := s.repository.GetJobByID(ctx, jobID)
if err != nil {
return nil, nil, err
}
keys, err := s.repository.GetJobKeys(ctx, jobID)
if err != nil {
return nil, nil, err
}
return j, keys, nil
}
func (s *service) GetJobKey(ctx context.Context, jobID uint, filename string) (*models.JobKey, error) {
return s.repository.GetJobKey(ctx, jobID, filename)
}
func (s *service) GetJobs(ctx context.Context, acoID uuid.UUID, statuses ...models.JobStatus) ([]*models.Job, error) {
jobs, err := s.repository.GetJobs(ctx, acoID, statuses...)
if err != nil {
return nil, err
}
if jobs == nil {
return nil, JobsNotFoundError{acoID, statuses}
}
return jobs, nil
}
type JobsNotFoundError struct {
ACOID uuid.UUID
StatusTypes []models.JobStatus
}
func (e JobsNotFoundError) Error() string {
return fmt.Sprintf("no Jobs found for acoID %s with job statuses %s",
e.ACOID, e.StatusTypes)
}
func (s *service) CancelJob(ctx context.Context, jobID uint) (uint, error) {
// Assumes the job exists and retrieves the job by ID
job, err := s.repository.GetJobByID(ctx, jobID)
if err != nil {
return 0, err
}
if job.Status == models.JobStatusPending || job.Status == models.JobStatusInProgress || job.Status == models.JobStatusCompleted {
job.Status = models.JobStatusCancelled
err = s.repository.UpdateJob(ctx, *job)
if err != nil {
return 0, ErrJobNotCancelled
}
return jobID, nil
}
// Return 0, ErrJobNotCancellable when attempting to cancel a non-cancellable job.
return 0, ErrJobNotCancellable
}
// createQueueJobs expands a single "prepare job" request into concrete queue jobs by:
// - choosing the effective resource + data types for the ACO/config (including BFD v3 rules)
// - splitting beneficiaries into batches sized for the target resource type
// - emitting one queue job per (resourceType × dataType × beneficiaryBatch)
func (s *service) createQueueJobs(ctx context.Context, args worker_types.PrepareJobArgs, since time.Time, beneficiaries []*models.CCLFBeneficiary) (jobs []*worker_types.JobEnqueueArgs, err error) {
sinceArg := formatSinceArg(since)
resourceTypes, effectiveDataTypes, err := s.getEffectiveQueueJobConfig(args)
if err != nil {
return nil, err
}
for _, resourceType := range resourceTypes {
maxBeneficiaries, err := getMaxBeneCount(resourceType)
if err != nil {
return nil, err
}
for _, beneficiaryIDs := range chunkBeneficiaryIDs(beneficiaries, maxBeneficiaries) {
resourceJobs, err := s.createJobsForResourceChunk(ctx, args, sinceArg, resourceType, beneficiaryIDs, effectiveDataTypes)
if err != nil {
return nil, err
}
jobs = append(jobs, resourceJobs...)
}
}
return jobs, nil
}
// formatSinceArg converts a time into the "_lastUpdated" query parameter format expected by Blue Button.
// A zero time means "no since filter", which is represented as the empty string.
func formatSinceArg(since time.Time) string {
if since.IsZero() {
return ""
}
// Persist in format ready for usage with _lastUpdated -- i.e., prepended with 'gt'.
return "gt" + since.Format(time.RFC3339Nano)
}
// getEffectiveQueueJobConfig determines which resource types should be queued and which claim
// data types should be requested, based on the matching ACO config and BFD API version.
func (s *service) getEffectiveQueueJobConfig(args worker_types.PrepareJobArgs) ([]string, []string, error) {
acoCfg, ok := s.GetACOConfigForID(args.CMSID)
if !ok {
return nil, nil, &bcdaerrors.InvalidACOConfigError{CMSID: args.CMSID}
}
resourceTypes := append([]string(nil), args.ResourceTypes...)
effectiveDataTypes := append([]string(nil), acoCfg.Data...)
if args.BFDPath != constants.BFDV3Path {
return resourceTypes, effectiveDataTypes, nil
}
excluded := slices.Contains(s.v3NoPartialClaimsModels, acoCfg.Model)
if !excluded && !slices.Contains(acoCfg.Data, constants.PartiallyAdjudicated) {
effectiveDataTypes = append(effectiveDataTypes, constants.PartiallyAdjudicated)
}
return resourceTypes, effectiveDataTypes, nil
}
// chunkBeneficiaryIDs splits beneficiaries into fixed-size ID batches for queueing.
func chunkBeneficiaryIDs(beneficiaries []*models.CCLFBeneficiary, maxBeneficiaries int) [][]string {
if len(beneficiaries) == 0 {
return nil
}
chunks := make([][]string, 0, (len(beneficiaries)+maxBeneficiaries-1)/maxBeneficiaries)
currentChunk := make([]string, 0, maxBeneficiaries)
for _, beneficiary := range beneficiaries {
currentChunk = append(currentChunk, fmt.Sprint(beneficiary.ID))
if len(currentChunk) < maxBeneficiaries {
continue
}
chunks = append(chunks, currentChunk)
currentChunk = make([]string, 0, maxBeneficiaries)
}
if len(currentChunk) > 0 {
chunks = append(chunks, currentChunk)
}
return chunks
}
// createJobsForResourceChunk produces enqueue arguments for a single resource type and beneficiary batch,
// creating a distinct job per compatible claim data type.
func (s *service) createJobsForResourceChunk(ctx context.Context, args worker_types.PrepareJobArgs, sinceArg string, resourceType string, beneficiaryIDs []string, effectiveDataTypes []string) ([]*worker_types.JobEnqueueArgs, error) {
resource, ok := GetClaimType(resourceType)
if !ok {
// This should never be possible, would have returned earlier.
return nil, errors.New("Invalid resource type: " + resourceType)
}
jobs := make([]*worker_types.JobEnqueueArgs, 0, len(effectiveDataTypes))
for _, dataType := range effectiveDataTypes {
if !resource.SupportsClaimType(dataType) {
continue
}
enqueueArgs, err := s.buildQueueJobArgs(ctx, args, sinceArg, beneficiaryIDs, resourceType, dataType)
if err != nil {
return nil, err
}
jobs = append(jobs, enqueueArgs)
}
return jobs, nil
}
func (s *service) buildQueueJobArgs(ctx context.Context, args worker_types.PrepareJobArgs, sinceArg string, beneficiaryIDs []string, resourceType string, dataType string) (*worker_types.JobEnqueueArgs, error) {
jobID, err := safecast.ToInt(args.Job.ID)
if err != nil {
log.API.Errorln(err)
}
enqueueArgs := worker_types.JobEnqueueArgs{
ID: jobID,
ACOID: args.ACOID.String(),
CMSID: args.CMSID,
BeneficiaryIDs: beneficiaryIDs,
ResourceType: resourceType,
Since: sinceArg,
TypeFilter: args.TypeFilter,
TransactionID: ctx.Value(middleware.CtxTransactionKey).(string),
TransactionTime: getQueueJobTransactionTime(args, dataType),
BBBasePath: args.BFDPath,
DataType: dataType,
}
if !s.setClaimsDate(&enqueueArgs, args) {
return nil, &bcdaerrors.InvalidACOConfigError{CMSID: args.CMSID}
}
return &enqueueArgs, nil
}
func getQueueJobTransactionTime(args worker_types.PrepareJobArgs, dataType string) time.Time {
// Partially-adjudicated data can lag adjudicated claims ingestion, so use job creation time.
if dataType == constants.PartiallyAdjudicated {
return args.CreationTime
}
return args.Job.TransactionTime
}
// Returns the beneficiaries associated with the latest CCLF file for the given request conditions,
// split between existing beneficiaries and newly-attributed beneficiaries.
func (s *service) getNewAndExistingBeneficiaries(ctx context.Context, args worker_types.PrepareJobArgs) (newBeneficiaries, beneficiaries []*models.CCLFBeneficiary, err error) {
cclfFileNew, err := s.repository.GetCCLFFileByID(ctx, args.CCLFFileNewID)
if err != nil {
return nil, nil, err
}
if cclfFileNew == nil {
return nil, nil, fmt.Errorf("no CCLF8 file found for cmsID %s", args.CMSID)
}
if !args.Since.IsZero() && cclfFileNew.CreatedAt.Sub(args.Since) < 0 {
// Retrieve all of the benes associated with this CCLF file.
benes, err := s.getBenesByFileID(ctx, cclfFileNew.ID, args)
if err != nil {
return nil, nil, err
}
if len(benes) == 0 {
return nil, nil, fmt.Errorf("found 0 new beneficiaries from CCLF8 file for cmsID %s cclfFiledID %d", args.CMSID, cclfFileNew.ID)
}
return newBeneficiaries, benes, nil
}
cclfFileOld, err := s.repository.GetCCLFFileByID(ctx, args.CCLFFileOldID)
if err != nil {
return nil, nil, err
}
if cclfFileOld == nil {
s.logger.Infof("Unable to find CCLF8 File for cmsID %s prior to date: %s; all beneficiaries will be considered NEW", args.CMSID, args.Since)
newBeneficiaries, err = s.getBenesByFileID(ctx, cclfFileNew.ID, args)
if err != nil {
return nil, nil, err
}
if len(newBeneficiaries) == 0 {
return nil, nil, fmt.Errorf("found 0 new beneficiaries from CCLF8 file for cmsID %s cclfFiledID %d", args.CMSID, cclfFileNew.ID)
}
return newBeneficiaries, nil, nil
}
oldMBIs, err := s.repository.GetCCLFBeneficiaryMBIs(ctx, cclfFileOld.ID)
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve MBIs for cmsID %s cclfFileID %d %s", args.CMSID, cclfFileOld.ID, err.Error())
}
// Retrieve all of the benes associated with this CCLF file.
benes, err := s.getBenesByFileID(ctx, cclfFileNew.ID, args)
if err != nil {
return nil, nil, err
}
if len(benes) == 0 {
return nil, nil, fmt.Errorf("found 0 new or existing beneficiaries from CCLF8 file for cmsID %s cclfFiledID %d", args.CMSID, cclfFileNew.ID)
}
// Split the results beteween new and old benes based on the existence of the bene in the old map
oldMBIMap := make(map[string]struct{}, len(oldMBIs))
for _, oldMBI := range oldMBIs {
oldMBIMap[oldMBI] = struct{}{}
}
for _, bene := range benes {
if _, ok := oldMBIMap[bene.MBI]; ok {
beneficiaries = append(beneficiaries, bene)
} else {
newBeneficiaries = append(newBeneficiaries, bene)
}
}
return newBeneficiaries, beneficiaries, nil
}
func (s *service) getBeneficiaries(ctx context.Context, args worker_types.PrepareJobArgs) ([]*models.CCLFBeneficiary, error) {
cclfFile, err := s.repository.GetCCLFFileByID(ctx, args.CCLFFileNewID)
if err != nil {
return nil, err
}
if cclfFile == nil {
return nil, fmt.Errorf("no CCLF8 file found for cmsID %s", args.CMSID)
}
benes, err := s.getBenesByFileID(ctx, cclfFile.ID, args)
if err != nil {
return nil, err
}
if len(benes) == 0 {
return nil, fmt.Errorf("found 0 beneficiaries from CCLF8 file for cmsID %s cclfFiledID %d", args.CMSID, cclfFile.ID)
}
return benes, nil
}
func (s *service) getBenesByFileID(ctx context.Context, cclfFileID uint, args worker_types.PrepareJobArgs) ([]*models.CCLFBeneficiary, error) {
var (
ignoredMBIs []string
err error
)
if !s.sp.includeSuppressedBeneficiaries {
upperBound := args.OptOutDate
if args.OptOutDate.IsZero() {
upperBound = time.Now()
}
if cfg, ok := s.GetACOConfigForID(args.CMSID); ok {
if !cfg.IgnoreSuppressions {
ignoredMBIs, err = s.repository.GetSuppressedMBIs(ctx, s.sp.lookbackDays, upperBound)
if err != nil {
return nil, fmt.Errorf("failed to retreive suppressedMBIs %s", err.Error())
}
}
} else {
return nil, &bcdaerrors.InvalidACOConfigError{CMSID: args.CMSID}
}
}
benes, err := s.repository.GetCCLFBeneficiaries(ctx, cclfFileID, ignoredMBIs)
if err != nil {
return nil, fmt.Errorf("failed to get beneficiaries %s", err.Error())
}
return benes, nil
}
// setClaimsDate computes the claims window to apply on the args
func (s *service) setClaimsDate(args *worker_types.JobEnqueueArgs, prepareArgs worker_types.PrepareJobArgs) bool {
// If the caller made a request for runout data
// it takes precedence over any other claims date
// that may be applied
if prepareArgs.RequestType == constants.Runout {
args.ClaimsWindow.Latest = s.rp.claimThruDate
} else if !prepareArgs.ClaimsDate.IsZero() {
args.ClaimsWindow.Latest = prepareArgs.ClaimsDate
}
// Applies the earliest boundary (lower bound) from the first matching ACOConfig
cfg, ok := s.GetACOConfigForID(prepareArgs.CMSID)
if ok {
args.ClaimsWindow.Earliest = cfg.LookbackTime(prepareArgs.CMSID)
}
return ok
}
// Gets the priority for the job where the lower the number the higher the priority in the queue.
// Priority is based on the request parameters that the job is executing on.
// Note: River queue library requires a priority between 1 and 4 (inclusive)
func (s *service) GetJobPriority(acoID string, resourceType string, sinceParam bool) int16 {
if isPriorityACO(acoID) {
return int16(1) // priority level for jobs for synthetic ACOs that are used for smoke testing
} else {
return int16(4) // default priority level for jobs
}
}
// GetACOConfigForID gets first matching currently loaded ACOConfig for the specified cmsID
func (s *service) GetACOConfigForID(cmsID string) (*ACOConfig, bool) {
for _, cfg := range s.acoConfigs {
if cfg.patternExp.MatchString(cmsID) {
return &cfg, true
}
}
return nil, false
}
// IsV3NoPartialClaimsModel checks if the provided model is listed in V3NoPartialClaimsModels
func (s *service) IsV3NoPartialClaimsModel(model string) bool {
return slices.Contains(s.v3NoPartialClaimsModels, model)
}
// Checks to see if an ACO is priority ACO based on a regex pattern provided by an
// environment variable.
func isPriorityACO(acoID string) bool {
if priorityACOPattern := conf.GetEnv("PRIORITY_ACO_REG_EX"); priorityACOPattern != "" {
priorityACORegex := regexp.MustCompile(priorityACOPattern)
if priorityACORegex.MatchString(acoID) {
return true
}
}
return false
}
func getMaxBeneCount(requestType string) (int, error) {
const (
BCDA_FHIR_MAX_RECORDS_EOB_DEFAULT = 50
BCDA_FHIR_MAX_RECORDS_PATIENT_DEFAULT = 5000
BCDA_FHIR_MAX_RECORDS_COVERAGE_DEFAULT = 4000
BCDA_FHIR_MAX_RECORDS_CLAIM_DEFAULT = 4000
BCDA_FHIR_MAX_RECORDS_CLAIMRESPONSE_DEFAULT = 4000
)
var envVar string
var defaultVal int
switch requestType {
case "ExplanationOfBenefit":
envVar = "BCDA_FHIR_MAX_RECORDS_EOB"
defaultVal = BCDA_FHIR_MAX_RECORDS_EOB_DEFAULT
case "Patient":
envVar = "BCDA_FHIR_MAX_RECORDS_PATIENT"
defaultVal = BCDA_FHIR_MAX_RECORDS_PATIENT_DEFAULT
case "Coverage":
envVar = "BCDA_FHIR_MAX_RECORDS_COVERAGE"
defaultVal = BCDA_FHIR_MAX_RECORDS_COVERAGE_DEFAULT
case "Claim":
envVar = "BCDA_FHIR_MAX_RECORDS_CLAIM"
defaultVal = BCDA_FHIR_MAX_RECORDS_CLAIM_DEFAULT
case "ClaimResponse":
envVar = "BCDA_FHIR_MAX_RECORDS_CLAIM_RESPONSE"
defaultVal = BCDA_FHIR_MAX_RECORDS_CLAIMRESPONSE_DEFAULT
default:
err := errors.New("invalid request type")
return -1, err
}
maxBeneficiaries := utils.GetEnvInt(envVar, defaultVal)
return maxBeneficiaries, nil
}
func (s *service) GetLatestCCLFFile(ctx context.Context, cmsID string, earlierTime time.Time, laterTime time.Time, fileType models.CCLFFileType) (*models.CCLFFile, error) {
cclfFile, err := s.repository.GetLatestCCLFFile(ctx, cmsID, constants.CCLF8FileNum, constants.ImportComplete, earlierTime, laterTime, fileType)
if err != nil {
return nil, err
}
if cclfFile == nil {
return nil, CCLFNotFoundError{8, cmsID, fileType, earlierTime}
}
return cclfFile, nil
}
// GetTimeConstraints searches for any time bounds that we should apply on the associated ACO
func (s *service) GetTimeConstraints(ctx context.Context, cmsID string) (TimeConstraints, error) {
var constraint TimeConstraints
aco, err := s.repository.GetACOByCMSID(ctx, cmsID)
if err != nil {
return constraint, fmt.Errorf("failed to retrieve aco: %w", err)
}
// If aco is not terminated, then we should not apply any time constraints
if aco.TerminationDetails == nil {
return constraint, nil
}
constraint.AttributionDate = aco.TerminationDetails.AttributionDate()
constraint.ClaimsDate = aco.TerminationDetails.ClaimsDate()
constraint.OptOutDate = aco.TerminationDetails.OptOutDate()
return constraint, nil
}
type CCLFNotFoundError struct {
FileNumber int
CMSID string
FileType models.CCLFFileType
CutoffTime time.Time
}
func (e CCLFNotFoundError) Error() string {
return fmt.Sprintf("no CCLF%d file found for cmsID %s fileType %d cutoffTime %s",
e.FileNumber, e.CMSID, e.FileType, e.CutoffTime.String())
}
var (
ErrJobNotCancelled = goerrors.New("job was not cancelled due to internal server error")
ErrJobNotCancellable = goerrors.New("job was not cancelled because it is not Pending or In Progress")
)

View File

@@ -0,0 +1,111 @@
aco_config:
# Kidney Care Choices (KCC) Model - Comprehensive Kidney Care Contracting
- model: 'CKCC' # Active, managed by 4i
name_pattern: '^C\d{4}$'
performance_year_transition: '01/01'
lookback_period: 2
data:
- 'adjudicated'
# Kidney Care Choices (KCC) Model - Kidney Care First
- model: 'KCF' # Ended, managed by 4i
name_pattern: '^K\d{4}$'
performance_year_transition: '01/01'
lookback_period: 2
data:
- 'adjudicated'
# ACO REACH Model, formerly Global and Professional Direct Contracting Model (GPDC) Model
- model: 'DC' # Active, managed by 4i
name_pattern: '^D\d{4}$'
performance_year_transition: '04/01'
lookback_period: 3
data:
- 'partially-adjudicated'
- 'adjudicated'
# Medicare Shared Savings Program
- model: 'SSP' # Active, managed by ACO-MS
name_pattern: '^A\d{4}$'
data:
- 'adjudicated'
# Comprehensive ESRD Care (CEC) Model
- model: 'CEC' # Ended
name_pattern: '^E\d{4}$'
data:
- 'adjudicated'
# Next Generation ACO Model
- model: 'NGACO' # Ended
name_pattern: '^V\d{3}$'
data:
- 'adjudicated'
# A config for BCDA's test entities
- model: 'TEST' # Active, managed by BCDA Admin
name_pattern: '^TEST\d{3}$'
data:
- 'adjudicated'
- 'partially-adjudicated'
# Maryland Total Cost of Care (MD TCoC) Model
- model: 'TCOCMD' # Ended, managed by BCDA Admin
name_pattern: '^CT\d{4,6}$'
data:
- 'adjudicated'
- 'partially-adjudicated'
attribution_file:
file_type: 'csv'
name_pattern: '(P|T)\.(PCPB)\.(M)([0-9][0-9])(\d{2})\.(D\d{6}\.T\d{6})\d'
model_identifier: 'PCPB'
file_performance_year: 4
file_date: 6
metadata_matches: 7
ignore_suppressions: true
# CMS Data Aggregation Contract
- model: 'CDAC' # Ended, managed by IHP
name_pattern: '^DA\d{4}$'
data:
- 'adjudicated'
- 'partially-adjudicated'
attribution_file:
file_type: 'csv'
name_pattern: '(P|T)\.(BCD)\.(DA\d{4}).(MBIY)(\d{2})\.(D\d{6}\.T\d{6})\d'
model_identifier: 'BCD'
file_performance_year: 5
file_date: 6
metadata_matches: 7
# GUIDE (Guiding an Improved Dementia Experience) Model
- model: 'GUIDE' # Active, managed by IHP
name_pattern: '^GUIDE-\d{4}$'
data:
- 'adjudicated'
attribution_file:
file_type: 'csv'
name_pattern: '(P|T)\.(GUIDE)\.(GUIDE-)(\d{4})\.(Y)(\d{2})\.(D\d{6}\.T\d{6})\d'
model_identifier: 'GUIDE'
file_performance_year: 6
file_date: 7
metadata_matches: 8
# ACCESS (Advancing Chronic Care with Effective, Scalable Solutions) Model
- model: 'ACCESS' # Active, managed by IHP
name_pattern: '^ACCES\d{5}$' # only one 's' due to character limits
lookback_period: 3
data:
- 'adjudicated'
attribution_file:
file_type: 'csv'
name_pattern: '(P|T)\.(ACCESS)\.(ACCES\d{5})\.(Y)(\d{2})\.(D\d{6}\.T\d{6})\d'
model_identifier: 'ACCESS'
file_performance_year: 5
file_date: 6
metadata_matches: 7
# Increasing Organ Transplant Access (IOTA) Model
- model: 'IOTA' # Active, managed via 4i
name_pattern: '^IOTA\d{3}$'
data:
- 'adjudicated'
- 'partially-adjudicated'
rate_limit_config:
all: true
acos: []
v1_v2_deny_regexes:
- '^IOTA\d{3}'
- '^ACCES\d{5}$'
- '^GUIDE-\d{4}$'
v3_no_partial_claims_models:
- 'GUIDE'

Binary file not shown.

121
dev/vendor/bcda-static-site/LICENSE vendored Normal file
View File

@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

View File

@@ -0,0 +1,22 @@
---
layout: announcement
page_title: "End of BCDA data limitations"
description: "Performance year (PY) 2026 data is now accessible."
show-side-nav: false
in-page-nav: true
lead_paragraph: "Performance year (PY) 2026 data now available."
published_date: 2026-02-12
---
All model entities can now access PY 2026 data using the `/Group/all` and `/Patient` endpoints.
- Use the `all` identifier for `/Group` and `/Patient` endpoint requests.
- Use the `all` identifier **for your first PY 2026 data request.** There will be no service date restrictions for this data.
## Requesting PY 2025 runout data
Use the `runout` identifier separately with the `/Group` endpoint. This will return data for enrollees who were attributed to your organization in PY 2025, but not PY 2026\.
- Use the `_since` parameter in conjunction with the `runout` identifier to limit data to updates occurring since your last runout request.
- The `runout` identifier will be updated each month up to and including July 2026.
- After July 2026, new data for enrollees who were attributed to your organization in PY 2025, but not PY 2026, will not be included.

View File

@@ -0,0 +1,57 @@
---
layout: announcement
page_title: "BCDA data limited in Jan 2026 for yearly updates"
description: "Claims with 2026 service dates will be unavailable throughout January 2026."
show-side-nav: false
in-page-nav: true
lead_paragraph: "Claims with 2026 service dates will be unavailable throughout January 2026."
published_date: 2025-12-03
---
Certain BCDA data is unavailable every January while CMS updates attributed beneficiaries for the new performance year (PY). Normal functionality will be restored to individual entities as soon as their data is ready. This typically takes 1 month. 
PY 2026 starts Jan 1, 2026 and ends Dec 31, 2026. 
## What data will be unavailable in Jan 2026?
Requests to `/Group/all` and `/Patient` endpoints will be disabled while we wait for updated attribution info for your model entity.
## How should I request PY 2025 runout data?
To request runout data for enrollees attributed to your organization in PY 2025:
* Use the `runout` identifier with the `/Group` endpoint. 
* Claims with a service date in 2026 will be excluded. Data returned will have a service date no later than December 31, 2025. 
* Use the [\_since parameter]({{ '/api-documentation/filter-claims-data.html#the-_since-parameter' | relative_url }}) with the `runout` identifier to return data updated since your last runout request. [Review our documentation on the runout identifier]({{ '/api-documentation/filter-claims-data.html#the-runout-identifier' | relative_url }}). 
## How will I know when data is available again?
We’ll notify you in the [BCDA Google Group](https://groups.google.com/g/bc-api) when the limitation ends for each supported model. At that point, you can use the `all` identifier to request data, including 2026 claims, with the `/Group` and `/Patient` endpoints as usual.
## Examples of requests/outcomes to expect in Jan 2026
- A caller makes a request to the `/Patient` or `/Group` endpoint using the `all` identifier.
- GET `/api/v2/Patient/$export`
- GET `/api/v2/Group/all/$export`
- The caller receives a `404 Not Found` response and an `OperationOutcome` error.
- An example `OperationOutcome.fhir.json` is attached.
- Callers should use the `/api/v2/Group/runout/$export` endpoint.
- A caller makes a request to the `/Group` endpoint using the `runout` identifier.
- GET `/api/v2/Group/runout/$export`
- The caller receives a `202 Accepted` response.
- Content-Location header sent with the job location.
- `Content-Location: /api/v2/jobs/{job_id}`
- A caller polls the `/Jobs` endpoint until the job completes.
- GET `/api/v2/jobs/{job_id}`
- The caller can now download the job data.
- GET `/data/2/output.json`
- All claims data will be capped at December 31, 2025.
## Need help?
Keep an eye on the [Google Group conversation](https://groups.google.com/g/bc-api/c/0v78i8P1sKo) to ask questions and read updates about BCDA data availability. If you can’t find what you need, send an email to [bcapi@cms.hhs.gov](mailto:bcapi@cms.hhs.gov). 

View File

@@ -0,0 +1,61 @@
---
layout: announcement
page_title: "BCDA v3: Enhanced timeliness and performance for value-based care"
description: "Core improvements to data sources and alignment mean timelier, more person-centered care. "
show-side-nav: false
in-page-nav: true
lead_paragraph: "Core improvements to data sources and alignment mean timelier, more person-centered care."
published_date: 2026-07-01
---
Centers for Medicare & Medicaid Services (CMS) is excited to announce version 3 (v3) of its [FHIR](http://hl7.org/fhir/) Medicare claims data sharing platform. v3 drives improvements to CMS Beneficiary Claims Data API (BCDA), [Blue Button API](https://bluebutton.cms.gov/), and [Claims Data to Part D Sponsors API (AB2D)](https://ab2d.cms.gov/). [BCDA v3]({{ '/about/introducing-v3.html' | relative_url }}) helps model entities coordinate care more effectively, track performance on CMS quality measures, and improve patient outcomes.
## BCDA v3 improvements
With BCDA v3, model entities get patient claims data earlier, allowing them to identify issues and intervene sooner. v3 strengthens alignment with Fast Healthcare Interoperability Resources (FHIR) standards and other CMS data sources, reducing time and money spent on integrations and reconciliation. Driving these improvements are:
- A new, single source of Medicare claims data
- Better alignment with sources such as Claim and Claim Line Feed (CCLF) files
These fundamental changes improve data frequency, tracking, filtering, mapping, and FHIR conformance.
### Timelier, more frequent updates
v3 offers timelier updates to patient and coverage data, meaning:
- Shorter time between full processing (adjudication) and data availability
- Increased frequency of updates to patient and coverage information
### Easier claims tracking
BCDA v3 makes it easier to track claims through the adjudication cycle. It uses one FHIR resource instead of 3 to represent fully and partially processed versions of a claim. v3 also simplifies the way BCDA denotes claims status.
### Enhanced filtering capabilities
The new [_typeFilter query parameter]({{ '/api-documentation/filter-claims-data.html#the-_typefilter-parameter' | relative_url }}) provides additional options for complex data workflows.
### Improved conformance with select FHIR Implementation Guides
v3 standardizes extensions while providing better FHIR conformance. CodeSystems are represented using `StructureDefinition` and `CodeSystem` resources.
### Simplified, reliable data mapping
BCDA v3 data aligns more closely with CCLF files, and generates its Data Dictionary automatically.
## The IDR: a single source of claims data
v3 consolidates upstream data to a single data source, the Integrated Data Repository (IDR). This change:
- Reduces mismatches between BCDA resources and CCLF files
- Resolves missing data for newly attributed enrollees
- Fixes issues for enrollees assigned more than one `BENE_ID`
- Simplifies linking between partially and fully adjudicated claims
- Uses consistent claim identifiers across all adjudication phases
## What this means for healthcare organizations
Through a unified data structure and consolidated data sources, BCDA v3 can reduce development time, improve data quality, and ensure providers have up-to-date information about every Medicare patient.
## What this means for current users
The v1 and v2 endpoints will be disabled on July 30, 2027. Refer to the [v3 Migration Guide]({{ '/api-documentation/how-to-migrate-v3.html' | relative_url }}) for steps and support on upgrading to v3.

View File

@@ -0,0 +1,23 @@
---
layout: announcement
page_title: "BCDA performance improvement to speed up job requests"
show-side-nav: false
in-page-nav: true
published_date: 2024-12-01
description: "Reducing resources file and increasing rate limits speeds up the time to complete long-running jobs with BCDA."
lead_paragraph: "New changes decrease the time it takes to complete long-running jobs. "
custom_excerpt: "New changes decrease the time it takes to complete long-running jobs. "
---
We've decreased the time it takes to complete long-running jobs by reducing the number of EOB resources per ndjson file. We've also increased the rate limit on API requests by 10X to accommodate the additional files.
What's changing?
- EOB resources per ndjson file are reduced from 200 to 50.
- We've increased the rate limit from 300 to 3,000 per 5 minutes from a single IP address.
Why is it changing?
- The change in EOB file volume decreases the time it takes to complete long-running jobs like requests for historical data.
- The API rate limit increase lowers likelihood of having your requests throttled.
What can you do to prepare?
- There's no action required. It can be helpful to revisit our guidance on [handling 429 errors]({{ "/api-documentation/access-claims-data.html#response-example-too-many-requests" | relative_url }}) caused by exceeding the API rate limit.

View File

@@ -0,0 +1,25 @@
---
layout: announcement
page_title: "BCDA v3: Expanded access, general availability"
description: "Over 600 new organizations eligible for BCDA in time for v3 general availability"
show-side-nav: false
in-page-nav: true
lead_paragraph: "Over 600 new organizations eligible for BCDA in time for v3 general availability"
published_date: 2026-10-07
---
Summer 2026 increased the reach of FHIR-enabled Medicare claims data thanks to new [Innovation Models](https://www.cms.gov/priorities/innovation/models) and the general availability release of BCDA v3.
## 3 new payment models can access BCDA
As of July 2026, participants in the [ACCESS](https://www.cms.gov/priorities/innovation/innovation-models/access), [GUIDE](https://www.cms.gov/priorities/innovation/innovation-models/guide), and [IOTA](https://www.cms.gov/priorities/innovation/innovation-models/iota) models have access to Medicare claims data. Timely claims data will help model participants treat and manage complex conditions. These include chronic conditions, dementia, and needs arising around kidney transplants.
## General availability means faster access to v3
With the phased rollout complete (from early adopter program, to limited release, to general availability), eligible organizations can now initiate their own access to BCDA v3. As announced in the [BCDA Google Group](https://groups.google.com/g/bc-api), you no longer need to submit a request to [onboard]({{ '/production-access.html' | relative_url }}) or [migrate]({{ '/api-documentation/how-to-migrate-v3.html' | relative_url }}) in order to get started.
## Why move to v3?
With v3, platform-level improvements lead to earlier access to claims data. Earlier access supports timely follow-ups and interventions. A closer alignment between v3 and other CMS data sources supports a more detailed, longitudinal picture of health for people with Medicare.
Join the [BCDA Google Group](https://groups.google.com/g/bc-api) for additional news and support.

View File

@@ -0,0 +1,612 @@
---
layout: api-docs
page_title: "How to Access Claims Data"
seo_title: ""
description: "Learn how to access Medicare enrollees' Parts A, B, and D claims data in the BCDA production environment for performance tracking and risk analysis."
in-page-nav: true
feedback_id: "f179685c"
---
# {{ page.page_title }}
Learn how to access claims data for both the sandbox and production environments and use Beneficiary Claims Data API (BCDA) endpoints.
The sandbox and production environments follow similar instructions. They support the same workflow, endpoints, parameters, and resource types.
<table class="usa-table usa-table--borderless margin-bottom-6">
<caption class="usa-sr-only">Sandbox and Production environments comparison</caption>
<thead>
<tr>
<th scope="col">Sandbox</th>
<th scope="col">Production</th>
</tr>
</thead>
<tbody>
<tr>
<td>Available to everyone via test credentials</td>
<td>Must <a href="{{ '/production-access.html' | relative_url }}">complete the steps</a> for production credentials</td>
</tr>
<tr>
<td>Contains test claims data</td>
<td>Contains real Medicare enrollee data</td>
</tr>
<tr>
<td><code>sandbox.bcda.cms.gov</code></td>
<td><code>api.bcda.cms.gov</code></td>
</tr>
</tbody>
</table>
<div class="usa-alert usa-alert--info">
<div class="usa-alert__body">
<p class="usa-alert__heading text-bold">BCDA recommends using v3 of the API</p>
<p class="usa-alert__text">This is the latest version which follows the <a href="https://hl7.org/fhir/R4/" target="_blank" rel="noopener noreferrer">FHIR R4 specification</a>.</p>
</div>
</div>
## Instructions
### 1. Get a bearer token
You will need a [bearer token]({{ '/api-documentation/get-a-bearer-token.html' | relative_url }}) to call the API. The process requires credentials, which are formatted as a client ID and client secret.
<div class="usa-alert usa-alert--warning usa-alert--slim">
<div class="usa-alert__body">
<p class="usa-alert__text">Jobs will not be interrupted when the bearer token <a href="{{ '/api-documentation/get-a-bearer-token.html#troubleshooting' | relative_url }}">expires</a>. In progress and queued jobs will continue to run.</p>
</div>
</div>
### 2. Start a job
Make a `GET` request to the /Group or /Patient endpoint to start a data export job. The examples below are sandbox curl requests to /Group. Follow along in your terminal or using a tool like Postman.
#### Request all resource types
By default, the `GET` request returns all available [resource types]({{ '/bcda-data.html#resource-types' | relative_url }}).
{% capture curlSnippet %}{% raw %}
GET /api/v3/Group/all/$export
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
Use the [_type parameter]({{ '/api-documentation/filter-claims-data.html' | relative_url }}) to specify which resource types you'd like returned.
#### Request header
The header must contain your bearer token. You may receive a `401` response if your credentials are invalid or expired. `Bearer ` must be included in the header with a capital B and followed by a space.
{% capture curlSnippet %}{% raw %}
Authorization: Bearer {bearer_token}
Accept: application/fhir+json
Prefer: respond-async
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="yaml" %}
#### Example curl commands to start a job
- Combine your `GET` request for resources with the request header.
- The dollar sign `$` before `export` in the URL indicates the endpoint is an <a href="https://hl7.org/fhir/R4/operations.html" target="_blank" rel="noopener noreferrer">operation</a>, rather than a CRUD interaction.
- PowerShell users will need to replace backslash characters `\` with backticks ( \` ) to properly escape the `$export` operation.
<div class="usa-alert usa-alert--info usa-alert--no-icon">
<div class="usa-alert__body">
<p class="usa-alert__text text-bold">Remember to use the correct URL for your environment</p>
<p class="usa-alert__text">Use <code>sandbox.bcda.cms.gov</code> to access the sandbox or <code>api.bcda.cms.gov</code> to access the production environment.</p>
</div>
</div>
<!-- snippet x3 -->
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/Group/all/\$export" \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/Group/all/\$export?_type=ExplanationOfBenefit,Patient" \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/Group/all/\$export?_type=Patient" \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
#### Response example: successful request
A `202 Accepted` response with a Content-Location header indicates a successful request.
#### Response header example
You'll need the job ID in the Content-Location header to check your job status.
{% capture curlSnippet %}{% raw %}
Content-Location: https://sandbox.bcda.cms.gov/api/v3/jobs/{job_id}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Response example: too many requests
A `429 Too Many Requests` response can occur due to 2 reasons:
1. Making too many HTTP requests within a period of time
2. Trying to recreate jobs already marked as "In-Progress.” For reference, you can view both existing and past jobs using the [/jobs endpoint]({{ '/api-documentation/access-claims-data.html' | relative_url }}#request-job-history).
Wait until the period of time specified in the `Retry-After: <delay-seconds>` header passes before making any more requests. This makes sure your client can adapt without manual intervention, even if the rate-limiting parameters change.
### 3. Check job status
Make a `GET` request to check the status using the job ID from step 2. You may need another bearer token if it's been over 20 minutes since it was generated.
#### Request to check the job status
{% capture curlSnippet %}{% raw %}
GET https://sandbox.bcda.cms.gov/api/v3/jobs/{job_id}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Request header
{% capture curlSnippet %}{% raw %}
Authorization: Bearer {bearer_token}
Accept: application/fhir+json
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="yaml" %}
#### curl command to check the job status
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/jobs/{job_id}" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
#### Response example: incomplete job
A `202` response indicates your job is still processing. The status will change to `200 OK` when the export is complete and the data is ready for download.
{% capture curlSnippet %}{% raw %}
202 Accepted
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Response header example: incomplete job
The X-Progress will have a percentage indicating your estimated progress.
{% capture curlSnippet %}{% raw %}
X-Progress: In Progress, 80%
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Response example: completed job
You'll receive a `200 OK` response with the output URL(s) needed to download the data. In the example URLs below, `42` indicates the job ID.
There is a separate URL for each resource type requested. The following example shows a request for all resource types.
{% capture curlSnippet %}{% raw %}
{
"transactionTime": "2026-12-09T20:44:01.705398Z",
"request": "https://sandbox.bcda.cms.gov/api/v3/Group/all/$export",
"requiresAccessToken": true,
"output": [
{
"type": "ExplanationOfBenefit",
"url": "https://sandbox.bcda.cms.gov/data/42/afd22dfa-c239-4063-8882-eb2712f9f638.ndjson"
},
{
"type": "Coverage",
"url": "https://sandbox.bcda.cms.gov/data/42/f76a0b76-48ed-4033-aad9-d3eec37e7e83.ndjson"
},
{
"type": "Patient",
"url": "https://sandbox.bcda.cms.gov/data/42/f92dcf16-63a2-448e-a12a-3bf677f966ed.ndjson"
}
],
"error": [],
"JobID": 42
}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="json" %}
### 4. Download the data
Make a `GET` request to download your data using the URL(s) from step 3.
If you're downloading from more than 1 URL, make multiple download requests concurrently to save time. Large files may take significantly longer to download.
<div class="usa-alert usa-alert--info usa-alert--no-icon">
<div class="usa-alert__body">
<p class="usa-alert__text text-bold">Files expire after 24 hours</p>
<p class="usa-alert__text">You can download these files using a new bearer token. After 24 hours, the files will expire and you will need to restart the job.</p>
</div>
</div>
#### Request to download the data
{% capture curlSnippet %}{% raw %}
GET https://sandbox.bcda.cms.gov/data/{job_id}/{file_name}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Request header: compressed data files
Request compressed data files with the optional `Accept-Encoding: gzip` header in your requests for faster download times. Afterward, decompress (unzip) the files into NDJSON format.
{% capture curlSnippet %}{% raw %}
Authorization: Bearer {bearer_token}
Accept-Encoding: gzip
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="yaml" %}
#### curl command to download the data
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/data/{job_id}/{file_name}" \
-H "Accept-Encoding: gzip" \
-H "Authorization: Bearer {bearer_token}"
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
If some of the data can't be exported due to errors, details can be found at the URLs in the error field. The errors are provided in an NDJSON file as FHIR OperationOutcome resources.
#### Response example
By default, you'll receive the requested data as FHIR resources in NDJSON format. Each resource type will appear as a separate, labeled file.
<div class="usa-alert usa-alert--info usa-alert--no-icon usa-alert--slim">
<div class="usa-alert__body">
<p class="usa-alert__text">Test data from the sandbox contains only negative Patient IDs.</p>
</div>
</div>
<ol>
<li><a href="{{ '/assets/downloads/v3_ExplanationOfBenefit.ndjson' | relative_url }}" data-tealium="download">ExplanationOfBenefit.ndjson {% include sprite.html icon="file_download" class="text-middle" size="2" %}</a></li>
<li><a href="{{ '/assets/downloads/v3_Patient.ndjson' | relative_url }}" data-tealium="download">Patient.ndjson {% include sprite.html icon="file_download" class="text-middle" size="2" %}</a></li>
<li><a href="{{ '/assets/downloads/v3_Coverage.ndjson' | relative_url }}" data-tealium="download">Coverage.ndjson {% include sprite.html icon="file_download" class="text-middle" size="2" %}</a></li>
</ol>
## Other BCDA endpoints
### Cancel a job
Cancel any active job. If the request is successful, you'll receive a `202` response.
#### Request to cancel a job
{% capture curlSnippet %}{% raw %}
DELETE /api/v3/jobs/{job_id}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Request header
{% capture curlSnippet %}{% raw %}
Authorization: Bearer {bearer_token}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="yaml" %}
#### curl command to cancel a job
{% capture curlSnippet %}{% raw %}
curl -X DELETE "https://sandbox.bcda.cms.gov/api/v3/jobs/{job_id}" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer {bearer_token}"
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
### Request job history
Retrieve details on your organization's historical requests, including the start and end datetime, unique ID, original valueString request, and status.
#### Request to retrieve all past jobs
If your organization has no jobs to return, you'll receive a `404 ERROR` response.
{% capture curlSnippet %}{% raw %}
GET /api/v3/jobs
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Request that filters jobs by end state
Supply the _status parameter to filter for jobs with a specific end state. BCDA jobs have 9 possible end states: Completed, Archived, Expired, Failed, FailedExpired, In Progress, Pending, Cancelled, and CancelledExpired.
These are how the job end states map to the 4 supported values you can receive in the response body:
- `Archived`, `Expired`, `Completed` → `completed`
- `FailedExpired`, `Failed` → `failed`
- `Pending`, `In Progress` → `in-progress`
- `CancelledExpired`, `Cancelled` → `cancelled`
The example below is a filtered request for all past archived jobs. If any are found, the response will list the status as `completed`. Even so, the filter will only return archived jobs; it will exclude expired and completed jobs.
{% capture curlSnippet %}{% raw %}
GET /api/v3/jobs?_status=Archived
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Request header
{% capture curlSnippet %}{% raw %}
Authorization: Bearer {bearer_token}
Accept: application/fhir+json
Prefer: respond-async
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="yaml" %}
#### curl command to check the job status
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/jobs" \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
#### Response example: completed job
The response will contain a bundle of resources for each historical job. Each resource section in the response represents a single past job request.
This example shows 1 historical job with a `completed` status. Since this was an unfiltered request, the job could either be archived, expired, or completed.
{% capture curlSnippet %}{% raw %}
{
"entry": [
{
"resource": {
"executionPeriod": {
"end": "2026-08-14T00:07:48+00:00",
"start": "2026-08-13T00:07:48+00:00"
},
"identifier": [
{
"system": "https://bcda.cms.gov/api/v3/jobs",
"use": "official",
"value": "1"
}
],
"input": [
{
"type": {
"text": "BULK FHIR Export"
},
"valueString": "GET https://bcda.test.gov/this-is-a-test"
}
],
"intent": "order",
"resourceType": "Task",
"status": "completed"
}
}
],
"resourceType": "Bundle",
"total": 1,
"type": "searchset"
}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="json" %}
### Request attribution status
Check your attribution status for a timestamp of when your attribution data was last updated. By comparing the timestamp to the date of your most recent job, you can determine if your organization has new claims data to download.
#### Request to check attribution status
{% capture curlSnippet %}{% raw %}
GET /api/v3/attribution_status
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Request header
{% capture curlSnippet %}{% raw %}
Authorization: Bearer {bearer_token}
Accept: application/json
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="yaml" %}
#### curl command to check attribution status
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/attribution_status" \
-H "Accept: application/json" \
-H "Authorization: Bearer {bearer_token}"
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
#### Response example
If BCDA has never ingested an attribution or runout file for your organization, you'll receive a `404 not found` response.
{% capture curlSnippet %}{% raw %}
{
"ingestion_dates": [
{
"type": "last_attribution_update",
"timestamp": "2020-12-22 22:31:40.397916+00"
},
{
"type": "last_runout_update",
"timestamp": "2020-12-22 22:31:40.397916+00"
}
]
}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="json" %}
### Check API status
Retrieve metadata to view the current status and release or FHIR version of the API. A bearer token, and therefore a response header, is not required.
#### Request to check API status
{% capture curlSnippet %}{% raw %}
GET /api/v3/metadata
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### curl command to check API status
{% capture curlSnippet %}{% raw %}
curl "https://sandbox.bcda.cms.gov/api/v3/metadata"
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
#### Response example
The response will contain a FHIR Capability Statement resource in JSON format. The example below shows the API is active, using FHIR Release 4.0.1, and that the API release version is r292.
{% capture curlSnippet %}{% raw %}
{
"resourceType": "CapabilityStatement",
"status": "active",
"date": "2026-07-17T16:20:37Z",
"publisher": "Centers for Medicare & Medicaid Services",
"kind": "instance",
"instantiates": [
"https://prod-sbx.fhir.bfd.cmscloud.local/v3/fhir/metadata",
"http://hl7.org/fhir/uv/bulkdata/CapabilityStatement/bulk-data"
],
"software": {
"name": "Beneficiary Claims Data API",
"version": "r292",
"releaseDate": "2026-07-17T16:20:37Z"
},
"implementation": {
"description": "The Beneficiary Claims Data API (BCDA) enables healthcare organizations participating in the Medicare Shared Savings Program or a CMS Innovation Model to retrieve Medicare Part A, Part B, and Part D claims data for their prospectively assigned or assignable beneficiaries.",
"url": "https://sandbox.bcda.cms.gov"
},
"fhirVersion": "4.0.1",
"format": [
"application/json",
"application/fhir+json"
],
"rest": [
{
"mode": "server",
"security": {
"cors": true,
"service": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/restful-security-service",
"code": "OAuth",
"display": "OAuth"
}
],
"text": "OAuth"
}
],
"extension": [
{
"url": "http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris",
"extension": [
{
"url": "token",
"valueUri": "https://sandbox.bcda.cms.gov/auth/token"
}
]
}
]
},
"interaction": [
{
"code": "batch"
},
{
"code": "search-system"
}
],
"resource": [
{
"type": "Patient",
"operation": [
{
"name": "export",
"definition": "http://hl7.org/fhir/uv/bulkdata/OperationDefinition/patient-export",
"documentation": "By default, the patient $export will return ExplanationOfBenefit resources with a meta.tag with a system of 'https://bluebutton.cms.gov/fhir/CodeSystem/System-Type' and code of either NationalClaimsHistory or DDPS. In order to return ExplanationOfBenefit resources with other system types (like SharedSystem), use the _typeFilter parameter."
}
],
"searchParam": [
{
"name": "_since",
"type": "date",
"documentation": "Return resources updated after the date provided for existing and newly attributed enrollees."
},
{
"name": "_type",
"type": "string",
"documentation": "Comma-delimited list of FHIR resource types to include in the export. By default, all supported resource types are returned."
},
{
"name": "_typeFilter",
"type": "string",
"documentation": "Use a URL-encoded FHIR subquery to further-refine patient export results."
}
]
},
{
"type": "Group",
"operation": [
{
"name": "export",
"definition": "http://hl7.org/fhir/uv/bulkdata/OperationDefinition/group-export",
"documentation": "By default, the group $export will return ExplanationOfBenefit resources with a meta.tag with a system of 'https://bluebutton.cms.gov/fhir/CodeSystem/System-Type' and code of either NationalClaimsHistory or DDPS. In order to return ExplanationOfBenefit resources with other system types (like SharedSystem), use the _typeFilter parameter."
}
],
"searchParam": [
{
"name": "_since",
"type": "date",
"documentation": "Return resources updated after the date provided for existing enrollees and all resources for newly attributed enrollees."
},
{
"name": "_type",
"type": "string",
"documentation": "Comma-delimited list of FHIR resource types to include in the export. By default, all supported resource types are returned."
},
{
"name": "_typeFilter",
"type": "string",
"documentation": "Use a URL-encoded FHIR subquery to further-refine group export results."
}
]
},
{
"type": "ExplanationOfBenefit",
"searchParam": [
{
"name": "_tag",
"type": "token",
"documentation": "Filter ExplanationOfBenefit by the meta.tag element. Pass full token as <system>|<code>. Supported codes in the 'https://bluebutton.cms.gov/fhir/CodeSystem/System-Type' system are: 'SharedSystem', 'NationalClaimsHistory', and 'DDPS'. By Default, only NationalClaimsHistory and DDPS claims will be returned."
},
{
"name": "outcome",
"type": "token",
"documentation": "Filter ExplanationOfBenefit by the outcome element. Supported values: 'partial' and 'complete'."
}
]
}
]
}
]
}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="json" %}

View File

@@ -0,0 +1,240 @@
---
layout: api-docs
page_title: "How to Filter Claims Data"
seo_title: ""
description: "Learn how to filter Medicare claims data requests using the _since, _type, and _typeFilter parameters."
in-page-nav: true
feedback_id: "72cee024"
tabs:
- label: v3
url: /api-documentation/filter-claims-data.html
- label: v2
url: /v2/api-documentation/filter-claims-data.html
---
# {{ page.page_title }}
{% include tabs.html items=page.tabs %}
BCDA offers 3 query parameters to filter or specify the resources returned:
- **_since:** applies a date boundary to your request
- **_type:** limits your request to 1 or more specific resource types
- **_typeFilter:** limits your request to resources matching the provided criteria
Use parameters in the sandbox or production environment to speed up download times and reduce file size.
## The _since parameter
The _since parameter lets you filter for claims data last updated after a specified date. Dates must be in the [FHIR standard instant format](https://www.hl7.org/fhir/datatypes.html#instant): `YYYY-MM-DDThh:mm:ss.sss+zz:zz`.
- Sample date: July 1, 2026 12:00 PM EST
- Formatted sample: `2026-07-01T12:00:00.000-05:00`
<div class="usa-alert usa-alert--warning usa-alert--no-icon">
<div class="usa-alert__body">
<p class="usa-alert__text text-bold">Requesting data from before 03/26/2026</p>
<p class="usa-alert__text">The FHIR resources available using BCDA v3 were first created in March 2026. Regardless of the service date, no resources from BCDA v3 will have a lastUpdated date earlier than March 2026.</p>
<p>If you specify a date prior to 03/26/2026 with the _since parameter, your request will include a large amount of additional historical data for your enrollees. Your request using _since with a date on or later than 03/26/2026 will include resources marked with appropriate lastUpdated dates.</p>
<p>If you wish to filter claims based on the date of service, use the _typeFilter parameter with service-date in your v3 request.</p>
</div>
</div>
We recommend new model entities run an unfiltered request for all historical data before using _since subsequently for incremental exports of new data. You can use the transactionTime from your most recent job as the specified date.
The _since parameter can be used with /Group or /Patient, but each endpoint will return data for newly attributed enrollees differently.
### Using _since with /Patient
Using _since with /Patient will return resources updated after the date provided for existing and newly attributed enrollees.
Newly attributed enrollees are those who’ve been assigned to your model entity since your last attribution date. If you don’t apply _since, BCDA will return data as early as March 1, 2021.
#### Example request using _since with /Patient
<!-- snippet -->
{% capture curlSnippet %}{% raw %}
GET /api/v3/Patient/$export?_type=Patient&_since=2026-07-01T08:00:00.000-05:00
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Example curl command using _since with /Patient
This command combines the `GET` request and request header. The dollar sign `$` before `export` in the URL indicates the endpoint is an action, not a resource.
<!-- snippet -->
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/Patient/\$export?_type=Patient&_since=2026-07-01T08:00:00.000-05:00" \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
### Using _since with /Group
Using _since with /Group will return resources updated after the date provided for existing enrollees and all resources for newly attributed enrollees.
This lets you retrieve all new claims data with a single request. If you don't apply _since, BCDA will return data as early as March 1, 2021.
#### Example request using _since with /Group
The request below will return:
- Claims data last updated since July 1, 2026 for existing enrollees
- Claims data as far back as 2014 for all new enrollees attributed to your organization in the last month
{% capture curlSnippet %}{% raw %}
GET /api/v3/Group/all/$export?_type=Patient&_since=2026-07-01T08:00:00.000-05:00
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Example curl command using _since with /Group
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/Group/all/\$export?_type=Patient&_since=2026-07-01T08:00:00.000-05:00" \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
## The _type parameter
The _type parameter lets you specify which resource types you'd like returned. Otherwise, making a `GET` request to start a job with the /Group or /Patient endpoint will return all resource types.
### Using _type
You'll need to use commas when specifying multiple resource types. The examples below are curl requests to /Group using _type.
#### Example request for 1 resource type
{% capture curlSnippet %}{% raw %}
GET /api/v3/Group/all/$export?_type=Patient
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Example request for 2 resource types
{% capture curlSnippet %}{% raw %}
GET /api/v3/Group/all/$export?_type=ExplanationOfBenefit,Patient
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Example curl command using _type
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/Group/all/\$export?_type=ExplanationOfBenefit,Patient" \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
## The _typeFilter parameter
<div class="usa-alert usa-alert--success usa-alert--no-icon">
<div class="usa-alert__body">
<p class="usa-alert__text text-bold font-ui-lg">Only available in v3</p>
<p class="usa-alert__text">The _typeFilter parameter is only available in v3.</p>
</div>
</div>
The _typeFilter parameter lets you use resource metadata to create finer-grained filtering criteria. BCDA will only return claims data that meets the specified criteria provided as a URL Encoded FHIR REST API query.
### Using _typeFilter
Currently, _typeFilter supports queries using the `_tag`, `outcome`, and `service-date` search parameters for the ExplanationOfBenefit resource:
* **_tag:** filters the export based on the `meta.tag` element. Pass a full token as `<system>|<code>`.
* Supported codes in the `https://bluebutton.cms.gov/fhir/CodeSystem/System-Type` system are:
* `SharedSystem`
* `NationalClaimsHistory`
* `DDPS`
* If you do not specify a System-Type _tag filter, the export will include only `NationalClaimsHistory` and `DDPS` claims by default.
* **outcome:** filters the export based on the processing status of the claim. The two values for this param, `partial` and `complete`, represent the "Partially Adjudicated" and "Fully Adjudicated" claim statuses.
* **service-date:** filters the export based on the claim's service date. The service date is the date that the care occurred within a billable period. This is a FHIR date param format (ex. `gt2026-01-14`)
#### Example request using _typeFilter
The request below will return:
- All Patient and Coverage resources
- ExplanationOfBenefit resources of fully adjudicated claims only
{% capture curlSnippet %}{% raw %}
GET /api/v3/Group/all/$export?_typeFilter=ExplanationOfBenefit%3Foutcome%3Dcomplete
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
#### Example curl command using _typeFilter
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/Group/all/\$export?_typeFilter=ExplanationOfBenefit%3Foutcome%3Dcomplete" \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
## The \`runout` identifier
The \`runout` identifier lets you request runouts data for enrollees attributed to your model entity the previous year, but not the current year. Claims data returned will have a service date no later than December 31 of the previous year.
### Using the `runout` identifier
The examples below are `GET` requests made to the /Group endpoint.
#### Example request for 1 resource type using `runout`
{% capture curlSnippet %}{% raw %}
GET /api/v3/Group/runout/$export?_type=Patient
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/Group/runout/\$export?_type=Patient" \
-H "accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
#### Example request for 2 resource types using `runout`
{% capture curlSnippet %}{% raw %}
GET /api/v3/Group/runout/$export?_type=ExplanationOfBenefit,Patient
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/Group/runout/\$export?_type=ExplanationOfBenefit,Patient" \
-H "accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}
#### Request for all resources using `runout`
{% capture curlSnippet %}{% raw %}
GET /api/v3/Group/runout/$export
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" %}
{% capture curlSnippet %}{% raw %}
curl -X GET "https://sandbox.bcda.cms.gov/api/v3/Group/runout/\$export" \
-H "accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Authorization: Bearer {bearer_token}" \
-i
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="shell" can_copy=true %}

View File

@@ -0,0 +1,345 @@
---
layout: api-docs
page_title: "How to Migrate to v3"
seo_title: "How to Migrate to BCDA v3 | CMS Beneficiary Claims Data API"
description: "Learn about new endpoints, FHIR resource consolidation, and updated URLs and IDs."
in-page-nav: true
feedback_id: "8a4c0b7b"
---
# {{ page.page_title }}
BCDA v3 became available July 1, 2026. Existing v1 and v2 users will need to migrate to v3 by July 30, 2027.
Along with background information, this guide:
- Introduces the [v3 Data Dictionary]({{ '/assets/downloads/BCDA_v3_Data_Dictionary.xlsx' | relative_url }})
- Helps you reformat [requests](#requesting-data-from-new-v3-endpoints) and [URLs](#new-extension-and-code-system-urls) in v3
- Introduces the [_typeFilter]({{ '/api-documentation/filter-claims-data.html#the-_typefilter-parameter' | relative_url }}) parameter
- Provides additional info on mapping and matching claims and variables between versions
## Migration overview
As you migrate from BCDA v1/v2 to v3, you’ll need to become familiar with changes including:
- New endpoint URL structure
- Unified ExplanationOfBenefit resource for all claims
- Updated extension and code system URLs
- Different resource IDs requiring new matching strategies
Please note that your initial v3 data request may take extra time. Additionally, as we continue to refine the v3 dataset, some fields will occasionally be re-loaded on our end. Corrected claims will show a more recent `last_updated` date and may result in larger than expected payloads. We will provide updates when these reloads are expected.
## BCDA v3 overview
BCDA v3 introduces access to more timely and accurate Medicare claims data and additional benefits to BCDA users including:
- More consistent claims data by using a single data source, the CMS Integrated Data Repository.
- Improved alignment with sources such as the Claim and Claim Line Feed (CCLF) files through more standard claim and patient identifiers.
### Changes to BCDA in v3
If you are currently using BCDA v1 or BCDA v2, there are changes to the API and FHIR Resources you need to be aware of in v3:
- There is a new v3 endpoint: `https://api.bcda.cms.gov/api/v3/`
- Partially adjudicated claims<sup><a href="#fn1">1</a></sup> will be represented as `ExplanationOfBenefit` (EOB) FHIR resources in v3, instead of `Claim`/`ClaimResponse` in v2
- There will be new Extension and Code System URLs to reference
- Resource IDs are different between versions
<p id="fn1" style="scroll-margin-top: 6.25rem;"><sup>1</sup> Medical claims that have been submitted but not fully processed and paid by Medicare.</p>
### v3 Data Dictionary
Download the <a href="{{ '/assets/downloads/BCDA_v3_Data_Dictionary.xlsx' | relative_url }}" data-tealium="download">BCDA v3 Data Dictionary</a> to learn about:
- updated information on resource type and claim field names
- updated mappings between CCLF and BCDA data
- new data available in v3
## Requesting data from new v3 endpoints
### Point your app to v3 instead of v2
BCDA v3 will be located at the same domain as v1 and v2. However, it will have new endpoints for metadata requests and the Group and Patient $export operations. BCDA v3 will still support the [Bulk Data FHIR Implementation Guide](https://hl7.org/fhir/uv/bulkdata/STU2/). If you are currently connected to BCDA v1 or v2 endpoints, you can migrate to BCDA v3 by making the following changes.
Point your app to **v3** instead of **v2** to connect to the new endpoint, complete the steps to export the data, check the job status, read the JSON manifest, and download the files.
- v2 Patient $export operation: 
{% include copy_snippet.html code="GET /api/v2/Group/all/$export" language="shell" %}
- v3 Patient $export operation: 
{% include copy_snippet.html code="GET /api/v3/Group/all/$export" language="shell" %}
The /auth and /data URLs will remain the same between versions. You won't need to make any changes to how you request a token or download files.
## Changes to claims representation
In BCDA v3, claims data are still returned in FHIR R4 format, but there are changes to the Patient, Coverage, and ExplanationOfBenefit resources. These changes include:
- Conformity with [CARIN Blue Button Implementation Guide (v2.2.0)](https://hl7.org/fhir/us/carin-bb/STU2.2/en/)
- New StructureDefenition and CodeSystem URLs for CMS-specific extensions and terminologies
- New data elements
Refer to the [v3 Data Dictionary]({{ '/api-documentation/how-to-migrate-v3.html#v3-data-dictionary' | relative_url }}) for a list of v3 supported data elements.
### Changes to partially adjudicated claims
BCDA v2 represents fully adjudicated claims by the ExplanationOfBenefit FHIR resource and partially adjudicated claims by the Claim and ClaimResponse resources.
BCDA v3 represents all claims (fully and partially adjudicated) with the ExplanationOfBenefit resource.
- v2 - `Claim`/`ClaimResponse` resource
- v3 - `ExplanationOfBenefit` resource
This change in v3 lets you compare claims at different stages in the adjudication process without mapping between resource types.
### Exporting claims based on adjudication status with v3
If you make requests for `Claim` and/or `ClaimResponse` resources using the `_type` parameter in v2, you'll need to update those requests in v3.
#### How it works in v2
In BCDA v2 you exported claims based on adjudication status using the `_type` parameter, and passing the appropriate resource types (`Claim`, `ClaimResponse` for partially adjudicated claims and `ExplanationOfBenefit` for fully adjudicated claims) as the parameter value:
- v2 $export for only partially adjudicated claims:
{% include copy_snippet.html code="GET /api/v2/Patient/$export?_type=Claim,ClaimResponse" language="shell" %}
- v2 $export for only fully adjudicated claims:
{% include copy_snippet.html code="GET /api/v2/Patient/$export?_type=ExplanationOfBenefit" language="shell" %}
In v2, BCDA differentiates "partially adjudicated" from "fully adjudicated" claims based on the data source rather than the claim status. BCDA's partially adjudicated data (Claim and ClaimResponse resources) are populated with data from the Medicare Shared Systems which often includes data for claims that had been fully processed and paid.
#### How it works in v3
We've extended the API with the [`_typeFilter` parameter]({{ '/api-documentation/filter-claims-data.html#the-_typefilter-parameter' | relative_url }}) to filter export data more granularly. Because all claims in v3 are represented by the same resource type (`ExplanationOfBenefit`), use this parameter to specify the System-Type _tag, recreating your v2 filtering logic.
Remember when using the _typeFilter parameter:
1. The _typeFilter parameter value must be URL-encoded
2. The _tag subquery parameter requires a token in the form `system|code`
{% capture sampleRequest %}{% raw %}
GET /api/v3/Patient/$export
?_type=ExplanationOfBenefit
&_typeFilter=ExplanationOfBenefit%3F_tag%3Dhttps%3A%2F%2Fbluebutton.cms.gov%2Ffhir%2FCodeSystem%2FSystem-Type%7CSharedSystem
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=sampleRequest language="shell" %}
**If you received `ExplanationOfBenefit`, `Claim`, and `ClaimResponse` resources in v2,** you received all claims.
In v3, specify all System-Type codes:
- SharedSystem
- NationalClaimsHistory
- DDPS
{% capture sampleRequest %}{% raw %}
GET /api/v3/Patient/$export
?_type=ExplanationOfBenefit
&_typeFilter=ExplanationOfBenefit%3F_tag%3Dhttps%3A%2F%2Fbluebutton.cms.gov%2Ffhir%2FCodeSystem%2FSystem-Type%7CSharedSystem%2Chttps%3A%2F%2Fbluebutton.cms.gov%2Ffhir%2FCodeSystem%2FSystem-Type%7CNationalClaimsHistory%2Chttps%3A%2F%2Fbluebutton.cms.gov%2Ffhir%2FCodeSystem%2FSystem-Type%7CDDPS
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=sampleRequest language="shell" %}
**If you used the _type parameter for `_type=Claim,ClaimResponse` in v2 requests,** you received SharedSystem claims only.
In v3, specify the SharedSystem System-Type code:
{% capture sampleRequest %}{% raw %}
GET /api/v3/Patient/$export
?_type=ExplanationOfBenefit
&_typeFilter=ExplanationOfBenefit%3F_tag%3Dhttps%3A%2F%2Fbluebutton.cms.gov%2Ffhir%2FCodeSystem%2FSystem-Type%7CSharedSystem
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=sampleRequest language="shell" %}
**If you used the _type parameter for `_type=ExplanationOfBenefit` for v2 requests,** you received NationalClaimsHistory claims and DDPS claims.
In v3, specify the NationalClaimsHistory and DDPS System-Type codes:
{% capture sampleRequest %}{% raw %}
GET /api/v3/Patient/$export
?_type=ExplanationOfBenefit
&_typeFilter=ExplanationOfBenefit%3F_tag%3Dhttps%3A%2F%2Fbluebutton.cms.gov%2Ffhir%2FCodeSystem%2FSystem-Type%7CNationalClaimsHistory%2Chttps%3A%2F%2Fbluebutton.cms.gov%2Ffhir%2FCodeSystem%2FSystem-Type%7CDDPS
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=sampleRequest language="shell" %}
##### Omitting _typeFilter
In v3, if you make a request without using the `_typeFilter` parameter to filter by System-Type, BCDA will return only NationalClaimsHistory and DDPS claims.
{% capture sampleRequest %}{% raw %}
GET /api/v3/Patient/$export?_type=ExplanationOfBenefit
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=sampleRequest language="shell" %}
## New extension and code system URLs
BCDA v3 uses the StructureDefinition and CodeSystem FHIR resources. This is the standard way to represent code system URLs. It makes it easier to access the metadata. You can access the metadata for each Extension and CodeSystem by hitting the URL. This will return a StructureDefinition or CodeSystem FHIR resource.
### URLs changing between v2 and v3
Make the following changes to migrate to v3 if you’ve mapped any bluebutton extension or code system URLs:
#### Changes to Extension URLs
<table class="usa-table usa-table--borderless usa-table--stacked margin-bottom-4">
<caption class="usa-sr-only">Changes to Extension URLs between v2 and v3</caption>
<tbody>
<tr>
<th scope="row">v2 URL</th>
<td>
{% include copy_snippet.html code="https://bluebutton.cms.gov/resources/variables/nch_near_line_rec_ident_cd/" language="ruby" can_copy=true %}
</td>
</tr>
<tr>
<th scope="row">v3 URL</th>
<td>
{% include copy_snippet.html code="https://bluebutton.cms.gov/fhir/StructureDefinition/CLM-NRLN-RIC-CD" language="ruby" can_copy=true %}
</td>
</tr>
</tbody>
</table>
#### Changes to CodeSystem URLs
<table class="usa-table usa-table--borderless usa-table--stacked margin-bottom-4">
<caption class="usa-sr-only">Changes to CodeSystem URLs between v2 and v3</caption>
<tbody>
<tr>
<th scope="row">v2 URL</th>
<td>
{% include copy_snippet.html code="https://bluebutton.cms.gov/resources/variables/nch_near_line_rec_ident_cd/" language="ruby" can_copy=true %}
</td>
</tr>
<tr>
<th scope="row">v3 URL</th>
<td>
{% include copy_snippet.html code="https://bluebutton.cms.gov/fhir/CodeSystem/CLM-NRLN-RIC-CD" language="ruby" can_copy=true %}
</td>
</tr>
</tbody>
</table>
If your BCDA client is using any of the v2 URLs, you’ll need to update your code to look for the v3 version.
## Different resource IDs between versions
<div class="usa-alert usa-alert--warning">
<div class="usa-alert__body">
<p class="usa-alert__heading text-bold">Do not use FHIR IDs to match resources between versions.</p>
<p class="usa-alert__text">To match <a href="{{ '/api-documentation/how-to-migrate-v3.html#matching-beneficiaries-between-v2-and-v3' | relative_url }}">beneficiaries</a>, use MBI and demographics data. To match <a href="{{ '/api-documentation/how-to-migrate-v3.html#matching-claims-between-v2-and-v3' | relative_url }}">claims</a>, use the claim control number.</p>
</div>
</div>
In BCDA v3, the FHIR ID (`Patient.id`, `ExplanationOfBenefit.id`, `Coverage.id`) for a given resource will not match the v2 FHIR resource ID.
**Example:**
If you receive the `Patient` resource from BCDA v3, do not look up an existing patient by expecting the v2 `Patient.id` to equal the v3 `Patient.id` value. These values will not match. They are a different set of identifiers. However, the range of v2 and v3 Patient IDs does overlap, so there are some IDs that represent a v2 Patient and v3 Patient. This would not mean they represent the same beneficiary in real life.  
Instead of matching on `Patient.id`, use the relevant CMS identifier (MBI), along with additional supporting resource info (patient demographics like name, date of birth, etc.). Similarly, `ExplanationOfBenefit.id` and Coverage.id will not match between v2 and v3. 
### Matching beneficiaries between v2 and v3
Because the v2 and v3 Patient IDs will not match, in order to match a v3 patient to a patient you already have in your database, the best way is to use the MBI and demographics. The MBI and demographics should match between versions of BCDA. 
To find the MBI in the FHIR Patient resource, look at the `Patient.identifier` element for an identifier where the `identifier.system` is `http://hl7.org/fhir/sid/us-mbi`. The `identifier.value` will be the MBI.
Example v3 `Patient.identifier` element:
{% capture curlSnippet %}{% raw %}
{
"identifier": [
{
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MB"
}
]
},
"system": "http://hl7.org/fhir/sid/us-mbi",
"value": "1S00E00AA08",
"period": {
"start": "2018-01-24T00:00:00+00:00"
}
}
]
}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="json" can_copy=true %}
It is always good practice to have some kind of validation based on patient demographics, like supplementing the MBI exact match with some subset of name, date of birth, and gender matching.
Once you have matched an existing `Patient`, or created a new one, it is ok to store the BCDA v3 `Patient.id`. It is possible for two different MBIs to reference the same beneficiary, and therefore the same BCDA v3 `Patient`. If that happens, you will see multiple instances of `Patient.identifier` where the system is `http://hl7.org/fhir/sid/us-mbi`. This is rare, but you should be prepared for it to happen.
Example of v3 `Patient.identifier` with two MBIs:
{% capture curlSnippet %}{% raw %}
{
"identifier": [
{
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MB"
}
]
},
"system": "http://hl7.org/fhir/sid/us-mbi",
"value": "1S00E00AA08",
"period": {
"start": "2022-04-05T00:00:00+00:00",
"end": "2022-04-12T00:00:00+00:00"
}
},
{
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MB"
}
]
},
"system": "http://hl7.org/fhir/sid/us-mbi",
"value": "1S00E00AE22",
"period": {
"start": "2022-04-12T00:00:00+00:00"
}
}
]
}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="json" can_copy=true %}
### Matching claims between v2 and v3
Because the v3 `ExplanationOfBenefit` FHIR ID will not match the v2 `ExplanationOfBenefit` or `Claim`/`ClaimResponse` FHIR IDs, in order to match a v3 claim to a claim you already have in your database, the best way is to use the claim control number.
Because each version of a claim will have a unique claim ID, you should avoid using the unique claim ID to track a claim across versions, or even within a version across the adjudication journey. Instead, use the claim control number. It is the identifier where the `identifier.system` equals `https://bluebutton.cms.gov/identifiers/CLM-CNTL-NUM`.
Example `ExplanationOfBenefit.identifier` element:
{% capture curlSnippet %}{% raw %}
{
"identifier": [
{
"type": {
"coding": [
{
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType",
"code": "uc",
"display": "Unique Claim ID"
}
]
},
"value": "-9422919134190"
},
{
"system": "https://bluebutton.cms.gov/identifiers/CLM-CNTL-NUM",
"value": "05968201612271KJS"
}
]
}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="json" can_copy=true %}
FHIRPath for claim identifier:
{% include copy_snippet.html code="ExplanationOfBenefit.identifier.where(system = 'https://bluebutton.cms.gov/identifiers/CLM-CNTL-NUM').value" language="ruby" can_copy=true %}

View File

@@ -0,0 +1,218 @@
---
layout: api-docs
page_title: "Comparison of BCDA v3 and CCLF Files"
seo_title: ""
description: ""
in-page-nav: true
tabs:
- label: v3
url: /bcda-data/comparison-bcda-cclf-files.html
- label: v2
url: /v2/bcda-data/comparison-bcda-cclf-files.html
---
# {{ page.page_title }}
{% include tabs.html items=page.tabs %}
Beneficiary Claims Data API (BCDA) and [Claim and Claim Line Feed (CCLF)](https://www.cms.gov/files/document/cclf-information-packet.pdf) files both offer Medicare Parts A, B, and D claims data. While the data sources overlap, there are some differences including their formatting and the frequency with which they make data available.
## Overview of similarities and differences
BCDA and CCLF files differ in how and when Medicare claims data is made available.
### BCDA: frequent access and standardization
As an API, BCDA lets you automate data requests and streamline workflows for more efficient, secure processes. The API uses Fast Healthcare Interoperability Resources ([FHIR](https://www.hl7.org/fhir/)) standards. It also supports frequent and early access to Medicare enrollees' claims data. This includes access to claims that would otherwise be unavailable during processing ("adjudication"). Earlier access to claims helps your organization build near real-time workflows and coordinate care for your patients.
FHIR breaks claims data into discrete blocks of recognizable data called `Resources`. These use information categories such as Patient, Explanation of Benefit, and Coverage to organize files, which make them easy to share and interpret. BCDA uses the [CMS recommended Bulk FHIR Standards](https://www.cms.gov/priorities/key-initiatives/burden-reduction/interoperability/implementation-guides-and-standards/standards-and-igs-index-and-resources) to export larger amounts of data across systems and populations.
### CCLF: ease of access and direct delivery
CCLF files provide adjudicated claims data delivered on a regular cadence, which may be useful for organizations with established file-based workflows or analyses that depend on fields unavailable in BCDA. You can download CCLF files directly from your model-specific portal.
## Who can use BCDA and CCLF files?
BCDA and CCLF claims data are available to organizations participating in some [Alternative Payment Models](https://www.cms.gov/priorities/innovation/key-concepts/alternative-payment-models-apms). These are programs that reward health care providers for high-quality, coordinated care. BCDA and CCLF files are currently available to organizations in these models:
- Medicare Shared Savings Program
- Realizing Equity, Access, and Community Health (ACO REACH)
- Kidney Care Choices (KCC)
- Increasing Organ Transplant Access (IOTA)
## Differences in access, format, content, and timing
{% assign unique = 1 %}
<table class="usa-table usa-table--stacked usa-table--borderless margin-bottom-0">
<thead>
<tr>
<th scope="col" id="col-bcda-{{unique}}">BCDA</th>
<th scope="col" id="col-cclf-{{unique}}">CCLF</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="colgroup" colspan="2" id="colgroup-{{unique}}">Access method</th>
</tr>
<tr>
<td headers="col-bcda-{{unique}} colgroup-{{unique}}" style="vertical-align: baseline; padding: 1rem; width:50%; border-bottom:0;">Programmatically using API requests</td>
<td headers="col-cclf-{{unique}} colgroup-{{unique}}" style="vertical-align: baseline; padding: 1rem; width:50%; border-bottom:0;">Manually using portals or programmatically using API requests or Command Line Interface</td>
</tr>
</tbody>
</table>
{% assign unique = 2 %}
<table class="usa-table usa-table--stacked usa-table--borderless margin-y-0">
<thead class="usa-sr-only">
<tr>
<th scope="col" id="col-bcda-{{unique}}">BCDA</th>
<th scope="col" id="col-cclf-{{unique}}">CCLF</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="colgroup" colspan="2" id="colgroup-{{unique}}">Claims data format</th>
</tr>
<tr>
<td headers="col-bcda-{{unique}} colgroup-{{unique}}" style="vertical-align: baseline; padding: 1rem; width:50%; border-bottom:0;">Interoperable and machine-readable, using <a href="https://hl7.org/fhir/R4/">FHIR R4</a>.</td>
<td headers="col-cclf-{{unique}} colgroup-{{unique}}" style="vertical-align: baseline; padding: 1rem; width:50%; border-bottom:0;">Human-readable, using <a href="https://www.cms.gov/files/document/cclf-information-packet.pdf">fixed-width tabular files</a>.</td>
</tr>
</tbody>
</table>
{% assign unique = 3 %}
<table class="usa-table usa-table--stacked usa-table--borderless margin-y-0">
<thead class="usa-sr-only">
<tr>
<th scope="col" id="col-bcda-{{unique}}">BCDA</th>
<th scope="col" id="col-cclf-{{unique}}">CCLF</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="colgroup" colspan="2" id="colgroup-{{unique}}">Claims data source</th>
</tr>
<tr>
<td headers="col-bcda-{{unique}} colgroup-{{unique}}" style="vertical-align: baseline; padding: 1rem; width:50%; border-bottom:0;">National Claims History and CMS Shared Systems data from the Integrated Data Repository (IDR)</td>
<td headers="col-cclf-{{unique}} colgroup-{{unique}}" style="vertical-align: baseline; padding: 1rem; width:50%; border-bottom:0;">National Claims History data from the Integrated Data Repository (IDR)</td>
</tr>
</tbody>
</table>
{% assign unique = 4 %}
<table class="usa-table usa-table--stacked usa-table--borderless margin-y-0">
<thead class="usa-sr-only">
<tr>
<th scope="col" id="col-bcda-{{unique}}">BCDA</th>
<th scope="col" id="col-cclf-{{unique}}">CCLF</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="colgroup" colspan="2" id="colgroup-{{unique}}">Historical data provided on newly attributed enrollees</th>
</tr>
<tr>
<td headers="col-bcda-{{unique}} colgroup-{{unique}}" style="vertical-align: baseline; padding: 1rem; width:50%; border-bottom:0;">
<ul style="margin: 0; padding-left: 1rem;">
<li>Shared Savings Program: all historical data available as far back as 2014</li>
<li>ACO REACH: 36 months of historical data from the start of the current performance year</li>
<li>KCC: 24 months of historical data from the start of the current performance year</li>
</ul>
</td>
<td headers="col-cclf-{{unique}} colgroup-{{unique}}" style="vertical-align: baseline; padding: 1rem; width:50%; border-bottom:0;">
<ul style="margin: 0; padding-left: 1rem;">
<li>Shared Savings Program: 36 months prior to agreement start date</li>
<li>ACO REACH: 36 months of historical data from the start of the current performance year</li>
<li>KCC: 36 months of historical data from the start of the current performance year</li>
</ul>
</td>
</tr>
</tbody>
</table>
{% assign unique = 5 %}
<table class="usa-table usa-table--stacked usa-table--borderless margin-y-0">
<thead class="usa-sr-only">
<tr>
<th scope="col" id="col-bcda-{{unique}}">BCDA</th>
<th scope="col" id="col-cclf-{{unique}}">CCLF</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="colgroup" colspan="2" id="colgroup-{{unique}}">Update frequency</th>
</tr>
<tr>
<td headers="col-bcda-{{unique}} colgroup-{{unique}}" style="vertical-align: baseline; padding: 1rem; width:50%; border-bottom:0;">
<ul style="margin: 0; padding-left: 1rem;">
<li>Adjudicated claims (Medicare Part A and Part B): weekly</li>
<li>Adjudicated claims (Medicare Part D): 6x/week</li>
<li>Partially adjudicated claims: 4 to 5x/week</li>
</ul>
</td>
<td headers="col-cclf-{{unique}} colgroup-{{unique}}" style="vertical-align: baseline; padding: 1rem; width:50%; border-bottom:0;">
<ul style="margin: 0; padding-left: 1rem;">
<li>Adjudicated claims: monthly</li>
<li>Adjudicated claim extracts: weekly<sup><a href="#fn1">*</a></sup></li>
</ul>
</td>
</tr>
</tbody>
</table>
<ul>
<li id="fn1" style="scroll-margin-top: 6.25rem;">CCLF Self-Service is only available to ACOs in the Medicare Shared Savings Program. Visit the <a href="https://acoms.cms.gov/knowledge-management/view/8321">ACO-MS Knowledge Library</a> to learn more about CCLF Self-Service.</li>
</ul>
## Which data source is right for me?
Both sources support model entities' use cases, and can even be used together. When choosing between them, consider the frequency, source, and level of automation most suited to your needs. You can also consider your organization's existing healthcare analytics and actuarial tools. A benefit of BCDA is its conformance with FHIR standards. This saves time and money on integrations with EHRs and off-the-shelf FHIR data clients.
BCDA aligns with CCLF partly by using shared claim identifiers. This alignment makes it easy to use both CCLF files and BCDA's standards-based format.
### Can I use both CCLF and BCDA as sources of Medicare claims data?
Yes. Using both data sources provides many benefits:
- Ensure data accuracy: Compare and cross-reference both sources to access a wider variety of historical data and identify any discrepancies.
- Evaluate care coordination and payment processes: Combine BCDA's insurance coverage details with CCLF files' payment data. This can evaluate the coordination of benefits between Medicare and other insurers, assess the financial impact on all parties, and identify primary or secondary payers.
## How soon will I receive claims data from BCDA v3 and CCLF files?
Depending on the model your organization participates in, claims data can be available:
- 2-4 days after the claim is **submitted to CMS** via BCDA v3
- 1-7 days after the claim is **adjudicated by CMS** via CCLF
The following diagram illustrates these differences while providing more detail about the claims process.
<figure class="width-full margin-y-4 margin-x-0" style="border: 1px solid #f0f0f0; border-radius: 4px; overflow:hidden; padding: 12px;">
<img
src="{{ '/assets/img/bcda-vs-cclf.svg' | relative_url }}"
alt="Timeline of when claim data becomes available to users through BCDA and CCLF after providers submit a claim."
class="width-full"
>
<figcaption class="usa-sr-only">
<ul>
<li>Through BCDA: a partially adjudicated claim is available 2 to 4 days after submission and continues to update every 2 to 4 days while CMS is processing. Once the claim is fully adjudicated, it is available through BCDA within 1 to 7 days.</li>
<li>Through CCLF: the fully adjudicated claim is available via CCLF Self-Service 1 to 7 days after adjudication and via the CCLF monthly feed 2 to 6 weeks after full adjudication.</li>
</ul>
</figcaption>
</figure>
## Which data fields in BCDA v3 are not available from CCLF files?
The BCDA v3 Data Dictionary contains a comprehensive list of all data fields available in BCDA v3 and includes a mapping to the equivalent or related CCLF file and element.
<p>For an exhaustive list of fields available only in BCDA v3, look for rows where the <code class="language-plaintext highlighter-rouge">cclfMapping</code> column is blank in the "Data Dictionary" worksheet in the <a href="{{ '/assets/downloads/BCDA_v3_Data_Dictionary.xlsx' | relative_url }}" data-tealium="download">BCDA v3 Data Dictionary {% include sprite.html icon="file_download" class="text-middle" size="2" %}</a>.</p>
## Which data fields in CCLF files are not available from BCDA v3?
The majority of data elements found in CCLF files are also available from BCDA v3. There are some elements not available from BCDA v3 including:
- Legacy HICN identifiers
- Certain beneficiary demographic information
- CCLF metadata
- Data fields from inactive programs
Review the "CCLF fields absent from BCDA v3" worksheet in the BCDA v3 Data Dictionary for a comprehensive list of fields found in CCLF files which are unavailable in BCDA v3.

View File

@@ -0,0 +1,231 @@
---
layout: api-docs
page_title: "Partially Adjudicated Claims Data"
seo_title: ""
description: "Get earlier access to Medicare enrollee data with BCDA partially adjudicated claims, now available as ExplanationOfBenefit resources in v3."
in-page-nav: true
feedback_id: "355f6ea6"
tabs:
- label: v3
url: /bcda-data/partially-adjudicated-claims-data.html
- label: v2
url: /v2/bcda-data/partially-adjudicated-claims-data.html
---
# {{ page.page_title }}
{% include tabs.html items=page.tabs %}
<p class="usa-intro">A faster way to access claims data</p>
Medicare claims processing (adjudication) can take weeks. Through partially adjudicated claims, ACO REACH participants (REACH ACOs) receive patient data only 2-4 days after providers submit claims to Medicare. This helps them provide patients with more effective interventions, transitions, and coordination of care.
Fully adjudicated claims offer the same rich insights, with some small differences including a longer frequency.
## Comparison: partially and fully adjudicated claims
<table class="usa-table usa-table--borderless usa-table--stacked margin-bottom-4">
<caption class="usa-sr-only">Partially versus fully adjudicated claims data</caption>
<thead>
<tr>
<th scope="col">Partially adjudicated claims</th>
<th scope="col">Fully adjudicated claims</th>
</tr>
</thead>
<tbody>
<tr scope="row">
<td data-label="Partially adjudicated claims">
Update daily
</td>
<td data-label="Fully adjudicated claims">
Update weekly
</td>
</tr>
<tr scope="row">
<td data-label="Partially adjudicated claims">
Available to all <a href="{{ '/index.html#eligible-model-entities' | relative_url }}">eligible model
entities</a>, except <a
href="https://www.cms.gov/priorities/innovation/innovation-models/guide"
target="_blank"
rel="noopener noreferrer"
>GUIDE</a> participants
</td>
<td data-label="Fully adjudicated claims">
Available to all <a href="{{ '/index.html#eligible-model-entities' | relative_url }}">eligible model
entities</a>
</td>
</tr>
<tr scope="row">
<td data-label="Partially adjudicated claims">
Requires BCDA v2<sup><a href="#fn1">*</a></sup> or v3
</td>
<td data-label="Fully adjudicated claims">
Available in all BCDA versions
</td>
</tr>
<tr scope="row">
<td data-label="Partially adjudicated claims">
Sourced from Medicare Shared Systems
</td>
<td data-label="Fully adjudicated claims">
Sourced from National Claims History (NCH)
</td>
</tr>
</tbody>
</table>
<ul>
<li id="fn1" style="scroll-margin-top: 6.25rem;">Access to partially adjudicated claims data using BCDA v2 is limited to REACH ACOs. Participants in other CMS Innovation Models and the Medicare Shared Savings Program must use BCDA v3 to access partially adjudicated claims data.</li>
</ul>
## What's in partially adjudicated claims data?
- In BCDA v3, partially adjudicated claims data:
- Are represented with 1 FHIR resource type:
- <a href="https://hl7.org/fhir/R4/explanationofbenefit.html" target="_blank" rel="noopener noreferrer">ExplanationOfBenefit (EOB)</a> - Information about the professional and institutional claims that providers submit for payment and information about a claim's adjudication status and processing results
- Contain Part A and Part B data from the current performance year and historical claims data from the previous 2 or more performance years, depending on your organization’s Alternative Payment Model.
- In BCDA v2, partially adjudicated claims data:
- Are represented with 2 FHIR resource types:
- <a href="https://hl7.org/fhir/R4/claim.html" target="_blank" rel="noopener noreferrer">Claim</a> - Information about the professional and institutional claims that providers submit for payment (including the services that enrollees receive)
- <a href="https://hl7.org/fhir/R4/claimresponse.html" target="_blank" rel="noopener noreferrer">ClaimResponse</a> - Information about a claim's adjudication status and processing results
- Contain Part A and Part B data (excluding Durable Medical Equipment) with updates from the past 60 days
## What's in fully adjudicated claims data?
In all versions of BCDA, adjudicated claims data:
- Are represented with 3 FHIR resource types:
- <a href="https://hl7.org/fhir/R4/patient.html" target="_blank" rel="noopener noreferrer">Patient</a> - Enrollees' demographic details and updates to their patient identifiers.
- <a href="https://hl7.org/fhir/R4/coverage.html" target="_blank" rel="noopener noreferrer"> Coverage</a> - Enrollees' insurance coverage details, including dual coverage.
- <a href="https://hl7.org/fhir/R4/explanationofbenefit.html" target="_blank" rel="noopener noreferrer"> ExplanationOfBenefit</a> - Details for episodes of care, including where and when the service was performed, diagnosis codes, provider, and cost of care.
- Contain Part A, Part B, and Part D data from the current performance year and historical claims data from the previous 2 or more performance years, depending on your organization's Alternative Payment Model
## How do I know when a claim has been fully processed?
All EOBs contain the <a href="https://hl7.org/fhir/R4/explanationofbenefit-definitions.html#ExplanationOfBenefit.outcome" target="_blank" rel="noopener noreferrer">ExplanationOfBenefit.outcome</a> element which will indicate if the claim is still processing. If you wish to include only EOBs that have been fully processed and paid in your job requests, see our guidance on <a href="{{ '/api-documentation/filter-claims-data.html#the-_typefilter-parameter' | relative_url }}">filtering claims by `ExplanationOfBenefit.outcome` using _typeFilter</a>.
### Matching multiple EOBs to the same claim
In v3, claims are automatically deduplicated within a source system. You won't need to deduplicate multiple versions of a claim within the same System-Type.
However, if you are receiving claims with the `SharedSystem` System-Type AND claims with the `NationalClaimsHistory` System-Type, then it is possible to receive two versions of the same claim as two different ExplanationOfBenefit resources.
To identify EOBs that represent the same claim, use the `CLM-CNTL-NUM` identifier `ExplanationOfBenefit.identifier.where('system'='https://bluebutton.cms.gov/fhir/CodeSystem/CLM-CNTL-NUM').value` to identify the matching pair. If two EOBs have the same Claim Control Number, then they represent the same claim.
Example identifier element with claim control number:
{% capture curlSnippet %}{% raw %}
"identifier": [
{
"system": "https://bluebutton.cms.gov/identifiers/CLM-CNTL-NUM",
"value": "-01614121123648SZF"
}
]
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="json" can_copy=true %}
There may be some instances in which a claim can't be de-duplicated via `CLM-CNTL-NUM`:
- #### Changes in Claim Control Number
If a claim receives a new claim control number, a previous claim control number will be available in `ExplanationOfBenefit.related`, and can be used to de-duplicate claims even if the claim control number has changed.
- #### PDE Claims
Because Medicare does not process Part D / Prescription claims directly, the rules to identify related claims are a bit different than Part A / Part B claims.
Part D claims are submitted to CMS by the Part D plan sponsors. As a result, not all Part D claims have a claim control number. However, those that do have a claim control number can be deduplicated using it.
For claims without a claim control number, you can use a natural key to link together related Part D Events for a given patient. The natural key logic is outlined in <a href="https://www.cms.gov/files/document/cclf-information-packet.pdf" target="_blank" rel="noopener">CCLF Information Packet</a> under Section 5.1.2, and repeated here:
- Line Item From Date (`CLM_LINE_FROM_DT`)
- Provider Service Identifier Qualifier Code (`PRVDR_SRVC_ID_QLFYR_CD`)
- Service Provider Generic ID Number (`CLM_SRVC_PRVDR_GNRC_ID_NUM`)
- RX Number (`CLM_LINE_RX_SRVC_RFRNC_NUM`)
- RX Fill Number (`CLM_LINE_RX_FILL_NUM`)
- #### DME Claims
When a claim from VMS is processed and released to the NCH, the leading digit (a 1) is dropped from the `CLM-CNTL-NUM`, due to a design decision made in preparation for Y2K.
<table class="usa-table usa-table--borderless margin-bottom-4">
<caption>Example</caption>
<thead>
<tr>
<th scope="col">VMS Claim Control Number</th>
<th scope="col">NCH Claim Control Number</th>
</tr>
</thead>
<tbody>
<tr>
<td>1222222222222222</td>
<td>222222222222222</td>
</tr>
</tbody>
</table>
### Tracking a Claim After it is Fully Processed
After a claim is fully adjudicated and released to the National Claims History system, it is still possible for updates to be made to the claim. Similar to matching ExplanationOfBenefit resources from different sources in the same export, you may need logic to match a claim from a current export to an existing claim from a previous export. Most of the time, you can still rely on the claim control number or the parent/original claim control number in `ExplanationOfBenefit.related`.
A couple of quirks to call out:
- **Professional claims:** NCH professional claims will never populate a new claim control number. Each new version of the claim will appear to have the original claim control number.
- **Institutional claims:** If an NCH institutional claim receives multiple rounds of claim control numbers, only the parent (the immediately-preceding claim control number) will be populated in `ExplanationOfBenefit.related`.
## Get started with partially adjudicated claims data
Access partially adjudicated claims data in the <a href="{{ '/api-documentation/get-a-bearer-token.html#sample-claims-data-sets' | relative_url }}">BCDA Sandbox</a>.
Review the <a href="{{ '/assets/downloads/BCDA_v3_Data_Dictionary.xlsx' | relative_url }}" data-tealium="download">Data Dictionary {% include sprite.html icon="file_download" class="text-middle"%}</a> for a complete list and description of the data fields.
## Use cases
### Example 1: Improve transition of care
<p class="usa-intro font-ui-md text-italic text-base">REACH ACOs can check if post-discharge processes are in place for
an attributed enrollee shortly after a hospital discharge. Benefits include reduced readmissions and healthcare costs.
</p>
On May 1, 2022, Mrs. Gonzales is doing better after a bout of pneumonia and is released home from the hospital. The hospital submits a claim for her stay to Medicare on May 3, 2022.
1. BCDA receives partially adjudicated claims data that reflects this update. This is identified by a Document Control Number (DCN) within a few days.
2. A REACH ACO sets up a dashboard to track newly identified discharges. After receiving an automated care-coordination alert for Mrs. Gonzales, they're able to follow up on her post-discharge care in a timely manner.
3. Using supplemental data, the REACH ACO adds filters to the dashboard for parameters of interest like whether the discharging hospital is in-network.
4. BCDA will reflect any status changes to the claim, for example if Mrs. Gonzales' claim is returned to the provider or adjusted.
5. These changes may involve updates to key variables used for care coordination, like diagnosis codes. The claims data will indicate the final status once a claim finishes adjudication and is submitted for payment.
6. BCDA's adjudicated data will pick up this claim after several additional processing steps. This is typically up to 14 days after adjudication.
REACH ACOs can also flag and identify patients who have a high risk of readmission based on factors like past diagnosis. Early access to claims data can help reduce readmissions and healthcare costs.
### Example 2: Identify opportunities for intervention
<p class="usa-intro font-ui-md text-italic text-base">REACH ACOs can monitor outpatient events for follow-up care and
track patterns that often indicate future utilization. The data can be used to target case management and deliver
clinically appropriate follow-up care. Additionally it can help reduce the volume of unnecessary procedures.</p>
On September 6, 2022, Mr. Fritz underwent a duplex scan to evaluate for carotid artery stenosis. The cardiology clinic submits a claim for this procedure to Medicare on September 8, 2022.
1. BCDA receives partially adjudicated claims data that reflects this update. This is identified by a Claim Control Number in a few days.
2. A REACH ACO creates a dashboard to track procedures that merit review. Mr. Fritz's service is flagged because it signals the surgeon may be planning a carotid endarterectomy procedure.
3. However, if Mr. Fritz does not have neurological symptoms, this procedure may not be a recommended course of action. The REACH ACO is able to activate protocols to alert Mr. Fritz's primary care provider for intervention.
4. As with example 1, BCDA will reflect any status changes to the claim.
5. Similarly, BCDA's adjudicated data will pick up this claim.
REACH ACOs can also track performance on cases where the care may be of limited value. For example, if Mr. Fritz does not have neurologic symptoms, then this kind of scan has been indicated to be more harmful than beneficial. This information could be used by the REACH ACO to reduce the volume of unnecessary procedures.
### Example 3: Enhance care coordination
<p class="usa-intro font-ui-md text-italic text-base">REACH ACOs can learn more about their patients' health histories, build their clinical profiles, and improve care coordination.</p>
Ms. Thompson began treatment for breast cancer on March 8, 2023. She is experiencing nausea and dehydration as a result of chemotherapy. While visiting family on March 14, she drove to an emergency room (ER) 50 miles from home. She was treated with IV fluids and nausea medication.
1. BCDA receives partially adjudicated claims data that reflects this update. This is identified by a Claim Control Number in a few days.
2. This emergency room visit may provide useful insights into Ms. Thompson's health history and chemotherapy journey. If Ms. Thompson is experiencing symptoms, her plan of treatment may need to be adjusted moving forward.
3. REACH ACOs can track procedures and services, including those from a different system, to get a holistic view of patients' health histories. They can then follow up with the patient and/or provider to improve care coordination.
REACH ACOs can also monitor the rate of emergency services used by patients undergoing active cancer treatment and following transplantation.
In the past, it would take weeks to know whether patients received care outside the network. Partially adjudicated claims data allows REACH ACOs to more quickly identify and investigate out-of-network care.
## Additional resources
- <a href="https://www.medicare.gov/what-medicare-covers" target="_blank" rel="noopener noreferrer">Services covered by Medicare Parts A and B</a>
- <a href="https://4innovation.cms.gov/secure/knowledge-management/view/341" target="_blank" rel="noopener noreferrer">Original Medicare (OM) Claims Processing</a> (requires 4i access)
- <a href="https://4innovation.cms.gov/secure/knowledge-management/view/491" target="_blank" rel="noopener noreferrer">Reporting and Data Sharing Overview</a> (requires 4i access)

View File

@@ -0,0 +1,156 @@
---
layout: support
page_title: "Frequently Asked Questions"
seo_title: ""
description: "Frequently asked questions about BCDA."
in-page-nav: true
---
# {{ page.page_title }}
## About the API
### Who is eligible to use Beneficiary Claims Data API (BCDA)?
BCDA supports organizations (entities) participating in the following CMS [alternative payment models](https://www.cms.gov/priorities/innovation/about/alternative-payment-models){:target="_blank" rel="noopener"}:
- [ACCESS (Advancing Chronic Care with Effective, Scalable Solutions)](https://www.cms.gov/priorities/innovation/innovation-models/access){:target="_blank" rel="noopener"}
- [ACO REACH (Accountable Care Organization Realizing Equity, Access, and Community Health)](https://www.cms.gov/priorities/innovation/innovation-models/aco-reach){:target="_blank" rel="noopener"}
- [GUIDE (Guiding an Improved Dementia Experience)](https://www.cms.gov/priorities/innovation/innovation-models/guide){:target="_blank" rel="noopener"}
- [IOTA (Increasing Organ Transplant Access)](https://www.cms.gov/priorities/innovation/innovation-models/iota){:target="_blank" rel="noopener"}
- [KCC (Kidney Care Choices)](https://www.cms.gov/priorities/innovation/innovation-models/kidney-care-choices-kcc-model){:target="_blank" rel="noopener"}
- [SSP (Shared Savings Program)](https://www.cms.gov/medicare/payment/fee-for-service-providers/shared-savings-program-ssp-acos){:target="_blank" rel="noopener"}
### When do terminated and discontinued model entities lose API access?
Terminated and discontinued organizations lose access to the API, including runouts data, the same day their participation in the model ends.
### What's the difference between BCDA and CCLF files?
CCLF files are automatically available monthly using 12 flat files, and can be downloaded weekly upon request.
BCDA updates adjudicated claims weekly and partially adjudicated claims 4x/week using 3 NDJSON files (Coverage, Patient, ExplanationOfBenefit).
Additionally, BCDA is an API that uses the [Bulk Fast Healthcare Interoperability Resources (FHIR)](https://hl7.org/fhir/uv/bulkdata/){:target="_blank" rel="noopener noreferrer"} format, as required by CMS. [Learn more about the differences]({{ '/bcda-data/comparison-bcda-cclf-files.html' | relative_url }}).
### What's the difference between BCDA v2 and v3?
BCDA v3 introduces a number of improvements, including:
- More frequent and timely updates
- Easier claims tracking
- Enhanced filtering capabilities
- Simplified, reliable data mapping capabilities
- Improved conformance with select FHIR Implementation Guides
- Simplified linking between partially and fully adjudicated claims
- Uses consistent claim identifiers across all phases of adjudication
We’ve also corrected issues some users encountered with v1 and v2 such as:
- Mismatched data between BCDA resources and CCLF files
- Missing data for newly attributed enrollees
- Issues for enrollees assigned more than one BENE_ID
You can learn more about v3 improvements and problems solved at [Introducing BCDA v3]({{ '/about/introducing-v3.html' | relative_url }}).
## Compliance and restrictions
{: .margin-bottom-2}
### How do I redact PHI and PII when sharing information?
Completely cover or label [Personally Identifiable Information (PII)](https://www.hhs.gov/answers/hhs-administrative/what-is-pii/index.html) and [Protected Health Information (PHI)](https://www.hhs.gov/answers/hipaa/what-is-phi/index.html) as “REDACTED” in emails, screenshots, and documents. Ensure any masks are 100% opaque and in a format that supports layers.
Examples of PII and PHI:
- Medicare Beneficiary Identifier (MBI)
- Taxpayer Identification Number (TIN)
- National Provider Identifier (NPI)
- Social Security Number (SSN)
- API keys or access credentials (e.g., client ID and secret)
- authorization or bearer tokens
PII and PHI are often found in API requests or response payloads referenced in:
- the text of your Google Group posts or emails
- files attached to your Google Group posts or emails (e.g., XMLs, JSONs)
- screenshots attached to your Google Group posts or emails
Example of a redacted response:
{% capture curlSnippet %}{% raw %}
{
"taxpayerIdentificationNumber": "REDACTED",
"nationalProviderIdentifier": "REDACTED"
}
{% endraw %}{% endcapture %}
{% include copy_snippet.html code=curlSnippet language="json" %}
### Can I use 3rd party web-based REST clients or tools?
Use caution with 3rd party web-based clients. By sharing your credentials, you may be allowing them to make REST calls on your behalf. This is a serious data privacy and security risk. We recommend using secure REST client tools.
## Claims processing timeline
### How long does it take BCDA to receive a claim after it is submitted?
It typically takes 2-4 days after submission for BCDA to receive [partially adjudicated claims data]({{ '/bcda-data/partially-adjudicated-claims-data.html' | relative_url }}) and up to 7 days for adjudicated claims data. Even after adjudication, claims may go through additional processing. BCDA will continue to provide the latest updates available for each claim.
According to Section 6404 of the Affordable Care Act, Original Medicare claims must be submitted within 12 months (1 calendar year) of the date of service.
### Where does the data come from?
BCDA v3 sources data from CMS's Integrated Data Repository (IDR).
For earlier versions of BCDA, adjudicated claims data is loaded from the [Chronic Conditions Data Warehouse (CCW)](https://www2.ccwdata.org/web/guest/home/){:target="_blank" rel="noopener noreferrer"}. Partially adjudicated claims data is loaded from the Fiscal Intermediary Standard System (FISS) and Multi-Carrier System (MCS).
### How often is data refreshed?
The following table shows how frequently Medicare claims data is refreshed and potentially available to eligible organizations.
<table class="usa-table usa-table--borderless usa-table--stacked margin-bottom-2">
<thead>
<tr>
<th scope="col">FHIR Resource</th>
<th scope="col">BCDA v3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Patient</th>
<td>6x/week, Sunday to Friday</td>
</tr>
<tr>
<th scope="row">Coverage</th>
<td>6x/week, Sunday to Friday</td>
</tr>
<tr>
<th scope="row">ExplanationOfBenefit - Part A/B (National Claims History)</th>
<td>Weekly, on Monday<sup><a href="#refresh-fn1">1</a></sup></td>
</tr>
<tr>
<th scope="row">ExplanationOfBenefit - Part D</th>
<td>5x/week, Sunday to Thursday</td>
</tr>
<tr>
<th scope="row">ExplanationOfBenefit - Part A/B (Shared Systems)</th>
<td>4x/week, Sunday and Tuesday to Thursday</td>
</tr>
</tbody>
</table>
<sup>1</sup> Data is loaded on the Monday after it's loaded into the source system, 0-7 days from processing or "adjudication."
{: #refresh-fn1 .font-body-2xs style="scroll-margin-top: 6.25rem;"}
To stay up to date with the latest claims data for your enrollees, we recommend exporting data from National Claims History once per week, and daily for all other sources. Use the [_since parameter]({{ '/api-documentation/filter-claims-data.html#the-_since-parameter' | relative_url }}) when running jobs to avoid downloading duplicate data.
## Troubleshooting
### Why do I get connection timeout issues with my production credentials?
BCDA requires that all production requests come from a registered IP address. Make sure the IP addresses you're using to request data have been added to the Allow List in your model-specific system. Visit [Production Access]({{ '/production-access.html' | relative_url }}) for more details.
### Why am I getting a 429 error response?
A status code of 429 indicates “Too Many Requests.” Wait until the period of time specified in the header has passed before making more requests.
This makes sure your client can adapt without manual intervention, even if the rate-limiting parameters change. [Learn more about the 429 status code.]({{ '/api-documentation/access-claims-data.html' | relative_url }}#response-example-too-many-requests)

View File

@@ -1,6 +1,6 @@
---
title: stack api serve
sidebar_position: 79
sidebar_position: 80
---
# `stack api serve`

View File

@@ -1,6 +1,6 @@
---
title: stack api
sidebar_position: 78
sidebar_position: 79
---
# `stack api`

View File

@@ -0,0 +1,20 @@
---
title: stack bcda errors
sidebar_position: 19
---
# `stack bcda errors`
```
Usage: stack bcda errors [OPTIONS] KEY
OperationOutcome issues from a job's error files (a partial export).
╭─ Arguments ──────────────────────────────────────────────────────────────────╮
│ * key TEXT Store job key. [required] │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --limit INTEGER Issues to print. [default: 50] │
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
```

View File

@@ -28,9 +28,13 @@ Usage: stack bcda export [OPTIONS]
│ --system-type TEXT _typeFilter System-Type │
│ tag(s). │
│ --outcome TEXT _typeFilter outcome: partial │
│ or complete. │
│ --service-date-from TEXT _typeFilter service-date > │
│ YYYY-MM-DD. │
│ and/or complete. │
│ --service-date-from TEXT _typeFilter earliest │
│ service-date (YYYY[-MM[-DD]], │
│ optional gt/ge/eq prefix). │
│ --service-date-to TEXT _typeFilter latest │
│ service-date (optional lt/le │
│ prefix). │
│ --per-type --one-job One job per type (survives │
│ sandbox sweeps). │
│ [default: per-type] │

View File

@@ -1,6 +1,6 @@
---
title: stack bcda load
sidebar_position: 19
sidebar_position: 20
---
# `stack bcda load`

View File

@@ -1,6 +1,6 @@
---
title: stack bcda sync
sidebar_position: 21
sidebar_position: 22
---
# `stack bcda sync`

View File

@@ -1,6 +1,6 @@
---
title: stack bcda vendor
sidebar_position: 20
sidebar_position: 21
---
# `stack bcda vendor`

View File

@@ -25,6 +25,8 @@ Usage: stack bcda [OPTIONS] COMMAND [ARGS]...
│ manifest. │
│ cancel Cancel an in-progress job. │
│ store Local store: jobs, files, bytes. │
│ errors OperationOutcome issues from a job's error files (a partial │
│ export). │
│ load Reconcile the store (or flatten the newest export per type), │
│ load, run the connector, publish the replica. │
│ vendor Vendored upstream (CMSgov/bcda-app, bcda.cms.gov): manifest, │

View File

@@ -1,6 +1,6 @@
---
title: stack bib attach-iom
sidebar_position: 34
sidebar_position: 35
---
# `stack bib attach-iom`

View File

@@ -1,6 +1,6 @@
---
title: stack bib attach-oig
sidebar_position: 39
sidebar_position: 40
---
# `stack bib attach-oig`

View File

@@ -1,6 +1,6 @@
---
title: stack bib backfill-comments
sidebar_position: 32
sidebar_position: 33
---
# `stack bib backfill-comments`

View File

@@ -1,6 +1,6 @@
---
title: stack bib cfr-jump
sidebar_position: 44
sidebar_position: 45
---
# `stack bib cfr-jump`

View File

@@ -1,6 +1,6 @@
---
title: stack bib code-tags
sidebar_position: 45
sidebar_position: 46
---
# `stack bib code-tags`

View File

@@ -1,6 +1,6 @@
---
title: stack bib discover-pfs-rules
sidebar_position: 28
sidebar_position: 29
---
# `stack bib discover-pfs-rules`

View File

@@ -1,6 +1,6 @@
---
title: stack bib fetch-docket-comments
sidebar_position: 29
sidebar_position: 30
---
# `stack bib fetch-docket-comments`

View File

@@ -1,6 +1,6 @@
---
title: stack bib fetch-pfs-comments
sidebar_position: 30
sidebar_position: 31
---
# `stack bib fetch-pfs-comments`

View File

@@ -1,6 +1,6 @@
---
title: stack bib fr-grab
sidebar_position: 42
sidebar_position: 43
---
# `stack bib fr-grab`

View File

@@ -1,6 +1,6 @@
---
title: stack bib fr-jump
sidebar_position: 43
sidebar_position: 44
---
# `stack bib fr-jump`

View File

@@ -1,6 +1,6 @@
---
title: stack bib import-zotero
sidebar_position: 36
sidebar_position: 37
---
# `stack bib import-zotero`

View File

@@ -1,6 +1,6 @@
---
title: stack bib ingest-iom
sidebar_position: 33
sidebar_position: 34
---
# `stack bib ingest-iom`

View File

@@ -1,6 +1,6 @@
---
title: stack bib ingest-mail
sidebar_position: 31
sidebar_position: 32
---
# `stack bib ingest-mail`

View File

@@ -1,6 +1,6 @@
---
title: stack bib ingest-oig
sidebar_position: 38
sidebar_position: 39
---
# `stack bib ingest-oig`

View File

@@ -1,6 +1,6 @@
---
title: stack bib query
sidebar_position: 27
sidebar_position: 28
---
# `stack bib query`

View File

@@ -1,6 +1,6 @@
---
title: stack bib refresh-iom
sidebar_position: 41
sidebar_position: 42
---
# `stack bib refresh-iom`

View File

@@ -1,6 +1,6 @@
---
title: stack bib refresh-oig
sidebar_position: 40
sidebar_position: 41
---
# `stack bib refresh-oig`

View File

@@ -1,6 +1,6 @@
---
title: stack bib sync-zotero
sidebar_position: 37
sidebar_position: 38
---
# `stack bib sync-zotero`

View File

@@ -1,6 +1,6 @@
---
title: stack bib sync
sidebar_position: 25
sidebar_position: 26
---
# `stack bib sync`

View File

@@ -1,6 +1,6 @@
---
title: stack bib tag
sidebar_position: 26
sidebar_position: 27
---
# `stack bib tag`

View File

@@ -1,6 +1,6 @@
---
title: stack bib watch-iom
sidebar_position: 35
sidebar_position: 36
---
# `stack bib watch-iom`

View File

@@ -1,6 +1,6 @@
---
title: stack bib
sidebar_position: 24
sidebar_position: 25
---
# `stack bib`

View File

@@ -1,6 +1,6 @@
---
title: stack bls ces
sidebar_position: 48
sidebar_position: 49
---
# `stack bls ces`

View File

@@ -1,6 +1,6 @@
---
title: stack bls oews
sidebar_position: 47
sidebar_position: 48
---
# `stack bls oews`

View File

@@ -1,6 +1,6 @@
---
title: stack bls
sidebar_position: 46
sidebar_position: 47
---
# `stack bls`

View File

@@ -1,6 +1,6 @@
---
title: stack comments dockets
sidebar_position: 53
sidebar_position: 54
---
# `stack comments dockets`

View File

@@ -1,6 +1,6 @@
---
title: stack comments extract-ocr
sidebar_position: 51
sidebar_position: 52
---
# `stack comments extract-ocr`

View File

@@ -1,6 +1,6 @@
---
title: stack comments extract
sidebar_position: 50
sidebar_position: 51
---
# `stack comments extract`

View File

@@ -1,6 +1,6 @@
---
title: stack comments seal
sidebar_position: 54
sidebar_position: 55
---
# `stack comments seal`

View File

@@ -1,6 +1,6 @@
---
title: stack comments stats
sidebar_position: 52
sidebar_position: 53
---
# `stack comments stats`

View File

@@ -1,6 +1,6 @@
---
title: stack comments unseal
sidebar_position: 55
sidebar_position: 56
---
# `stack comments unseal`

View File

@@ -1,6 +1,6 @@
---
title: stack comments
sidebar_position: 49
sidebar_position: 50
---
# `stack comments`

View File

@@ -1,6 +1,6 @@
---
title: stack db comment
sidebar_position: 72
sidebar_position: 73
---
# `stack db comment`

View File

@@ -1,6 +1,6 @@
---
title: stack db inspect
sidebar_position: 73
sidebar_position: 74
---
# `stack db inspect`

View File

@@ -1,6 +1,6 @@
---
title: stack db
sidebar_position: 71
sidebar_position: 72
---
# `stack db`

View File

@@ -1,6 +1,6 @@
---
title: stack docs build
sidebar_position: 75
sidebar_position: 76
---
# `stack docs build`

View File

@@ -1,6 +1,6 @@
---
title: stack docs generate
sidebar_position: 77
sidebar_position: 78
---
# `stack docs generate`

View File

@@ -1,6 +1,6 @@
---
title: stack docs serve
sidebar_position: 76
sidebar_position: 77
---
# `stack docs serve`

View File

@@ -1,6 +1,6 @@
---
title: stack docs
sidebar_position: 74
sidebar_position: 75
---
# `stack docs`

View File

@@ -1,6 +1,6 @@
---
title: stack generate models
sidebar_position: 23
sidebar_position: 24
---
# `stack generate models`

View File

@@ -1,6 +1,6 @@
---
title: stack generate
sidebar_position: 22
sidebar_position: 23
---
# `stack generate`

View File

@@ -1,6 +1,6 @@
---
title: stack lake deploy
sidebar_position: 57
sidebar_position: 58
---
# `stack lake deploy`

View File

@@ -1,6 +1,6 @@
---
title: stack lake load
sidebar_position: 58
sidebar_position: 59
---
# `stack lake load`

View File

@@ -1,6 +1,6 @@
---
title: stack lake parity
sidebar_position: 60
sidebar_position: 61
---
# `stack lake parity`

View File

@@ -1,6 +1,6 @@
---
title: stack lake validate
sidebar_position: 59
sidebar_position: 60
---
# `stack lake validate`

View File

@@ -1,6 +1,6 @@
---
title: stack lake
sidebar_position: 56
sidebar_position: 57
---
# `stack lake`

View File

@@ -1,6 +1,6 @@
---
title: stack llm eval-tags
sidebar_position: 68
sidebar_position: 69
---
# `stack llm eval-tags`

View File

@@ -1,6 +1,6 @@
---
title: stack llm hosts
sidebar_position: 64
sidebar_position: 65
---
# `stack llm hosts`

View File

@@ -1,6 +1,6 @@
---
title: stack llm index
sidebar_position: 62
sidebar_position: 63
---
# `stack llm index`

View File

@@ -1,6 +1,6 @@
---
title: stack llm prune-rules
sidebar_position: 69
sidebar_position: 70
---
# `stack llm prune-rules`

View File

@@ -1,6 +1,6 @@
---
title: stack llm restamp
sidebar_position: 63
sidebar_position: 64
---
# `stack llm restamp`

View File

@@ -1,6 +1,6 @@
---
title: stack llm serve
sidebar_position: 65
sidebar_position: 66
---
# `stack llm serve`

View File

@@ -1,6 +1,6 @@
---
title: stack llm stamp-themes
sidebar_position: 70
sidebar_position: 71
---
# `stack llm stamp-themes`

View File

@@ -1,6 +1,6 @@
---
title: stack llm tag
sidebar_position: 67
sidebar_position: 68
---
# `stack llm tag`

View File

@@ -1,6 +1,6 @@
---
title: stack llm vocab
sidebar_position: 66
sidebar_position: 67
---
# `stack llm vocab`

View File

@@ -1,6 +1,6 @@
---
title: stack llm
sidebar_position: 61
sidebar_position: 62
---
# `stack llm`

View File

@@ -1,6 +1,6 @@
---
title: stack mail attach-smarthost
sidebar_position: 120
sidebar_position: 121
---
# `stack mail attach-smarthost`

View File

@@ -1,6 +1,6 @@
---
title: stack mail dkim-export
sidebar_position: 119
sidebar_position: 120
---
# `stack mail dkim-export`

View File

@@ -1,6 +1,6 @@
---
title: stack mail dns
sidebar_position: 118
sidebar_position: 119
---
# `stack mail dns`

View File

@@ -1,6 +1,6 @@
---
title: stack mail down
sidebar_position: 116
sidebar_position: 117
---
# `stack mail down`

View File

@@ -1,6 +1,6 @@
---
title: stack mail provision
sidebar_position: 114
sidebar_position: 115
---
# `stack mail provision`

View File

@@ -1,6 +1,6 @@
---
title: stack mail rotate-creds
sidebar_position: 121
sidebar_position: 122
---
# `stack mail rotate-creds`

View File

@@ -1,6 +1,6 @@
---
title: stack mail seed-mailboxes
sidebar_position: 122
sidebar_position: 123
---
# `stack mail seed-mailboxes`

View File

@@ -1,6 +1,6 @@
---
title: stack mail status
sidebar_position: 117
sidebar_position: 118
---
# `stack mail status`

View File

@@ -1,6 +1,6 @@
---
title: stack mail up
sidebar_position: 115
sidebar_position: 116
---
# `stack mail up`

View File

@@ -1,6 +1,6 @@
---
title: stack mail wire-git
sidebar_position: 123
sidebar_position: 124
---
# `stack mail wire-git`

View File

@@ -1,6 +1,6 @@
---
title: stack mail
sidebar_position: 113
sidebar_position: 114
---
# `stack mail`

View File

@@ -1,6 +1,6 @@
---
title: stack perf show
sidebar_position: 81
sidebar_position: 82
---
# `stack perf show`

View File

@@ -1,6 +1,6 @@
---
title: stack perf
sidebar_position: 80
sidebar_position: 81
---
# `stack perf`

View File

@@ -1,6 +1,6 @@
---
title: stack pfs cpt-ingest
sidebar_position: 95
sidebar_position: 96
---
# `stack pfs cpt-ingest`

Some files were not shown because too many files have changed in this diff Show More