Appearance
CMS Marketplace API dependency map
This page documents where AskFlorence depends on the live CMS Marketplace API today vs where we serve from our own MongoDB. It also captures the forward plan for retiring remaining CMS dependencies if we ever need to (CMS outage, rate-limit pressure, performance issue, or strategic independence).
Bottom line as of 2026-05-14: We are not technically dependent on CMS for any compute in the consumer flow. We have the data and math in our system for everything except a few narrow edge cases. Remaining CMS calls are historical inertia from the pre-owned-data migration. Until we hit an issue, it's not worth the time to migrate.
Use this page as the reference when planning route changes, rate-limit posture, or CMS-outage incident response.
Current state — what calls CMS today
Traced end-to-end through the standard member smoke flow: home → calculator → takeover → /plans → drug + provider search → /plans/[planId].
Routes that hit CMS
| Route | When | What CMS provides | Why we still call it |
|---|---|---|---|
POST /api/eligibility | Calculator submit (federal states only) | APTC, CSR tier, Medicaid flag, hardship exemption, coverage gap flag | Historical. We have all the underlying data + math in our system. See "Migration plan: federal eligibility" below. |
POST /api/drugs/autocomplete | Every keystroke (3+ chars) on the /plans drug search input | Drug names → RxCUI codes | Historical + missing index. We have formularies_staging (RxCUI-keyed) but no name-indexed search built on top yet. |
POST /api/drugs/covered | When a drug is selected, batched across all visible plans | "Covered / NotCovered / PartialCovered / GenericCovered" + drug_tier per (rxcui, plan_id) | CMS primary, our DB gap-fill. lookupStagingDrugTiers() reads formularies_staging only when CMS returns "Covered without drug_tier". Our data is actually richer than CMS for tier info. |
POST /api/providers/autocomplete | Every keystroke (3+ chars) on the /plans provider search input. Fans out 3 calls (Individual + Facility + Group) | Provider names → NPI codes | Historical + missing index. We have providers_staging (NPI-keyed) but no name-indexed search built on top yet. |
POST /api/providers/covered | When a provider is selected, batched across all visible plans | "Covered / NotCovered / PartialCovered" + network_tier per (npi, plan_id) | CMS primary, our DB gap-fill. lookupStagingProviderNetworks() reads providers_staging only when CMS returns "Covered without network_tier". |
GET /api/counties?zip=... (fallback only) | Only when a zip isn't in our zip_county collection | County FIPS lookup → SBE-redirect detection for state-based marketplace ZIPs | Operational backstop. Mostly catches SBE-state ZIPs missing from our DB (e.g., a CA ZIP we haven't ingested). 99%+ of legitimate federal-state traffic stays in our DB. |
POST /api/plans (CMS branch — effectively dead) | Only when place.state is not in OWNED_DATA_STATES (the ~20 SBE states) | Plan search | Dead code path. Users for SBE states are caught by /api/counties sbeRedirect first; this branch is never reached in practice. |
Routes that DON'T hit CMS (served from our DB)
| Route | Served by | Notes |
|---|---|---|
GET /api/counties (happy path) | zip_county MongoDB collection | 100% of federal-30 + NY traffic. Multi-county ZIPs handled; whole-ZIP SBE redirect handled; cross-state border ZIPs handled. |
POST /api/eligibility (NY only) | calculateNyEligibility() in src/lib/owned-plans.ts | NY uses community rating + Essential Plan, our own math. This is the template proving we can do the same for federal states. |
POST /api/plans (NY + 30 federal states) | searchOwnedPlans() over plans MongoDB collection | Covers OWNED_DATA_STATES = NY + AK, AL, AR, AZ, DE, FL, HI, IA, IN, KS, LA, MI, MO, MS, MT, NC, ND, NE, NH, OH, OK, OR, SC, SD, TN, TX, UT, WI, WV, WY. Pricing math audit-locked vs CMS at tier-1 through tier-5. |
GET /plans/[planId] (server fetch of extras) | fetchPlanDetailExtras() reads puf.sbcScenarios + puf.qualityRating from plans collection | Phase A PUF ingest. No CMS call. |
All write paths (/api/waitlist, /api/agents/discovery*, /api/unsubscribe, /api/enroll/applications) | MongoDB (agent_waitlist_submissions, agent_survey_responses, etc.) | No CMS involvement. |
Per-flow CMS call count (typical Utah Medicaid scenario, member smoke flow)
| Step | CMS calls | Mongo queries |
|---|---|---|
| Counties lookup | 0 | 1 |
| Eligibility (UT → federal → CMS) | 1 | 0 |
| Plans (UT → owned data) | 0 | 1 |
| Takeover render | 0 | 0 |
| /plans page loads (re-runs eligibility + plans) | 1 | 1 |
| Doctor autocomplete ("Tyler" → 5 keystrokes after 3-char threshold) | 15 (5 × 3 fan-out) | 0 |
| Doctor coverage check (16 plans, 1 doctor) | 2 | 0 (or fallback only) |
| Drug autocomplete ("Synthroid") | 6 | 0 |
| Drug coverage check (16 plans, 1 drug) | 2 | 0 (or fallback only) |
| 2nd drug "Lipitor" autocomplete + coverage | 6 + 2 = 8 | 0 |
| 3rd drug "Ozempic" autocomplete + coverage | 6 + 2 = 8 | 0 |
| Plan detail page load (extras + re-run pipeline) | 1 | 1 |
| Total per smoke flow | ~43 CMS calls | ~4 Mongo queries |
For NY (owned-data state), the federal eligibility CMS call drops, taking it to ~42 CMS calls. The dominant CMS load is doctor + drug search, not eligibility or plans.
What is NOT in CMS (and therefore CMS-independent already)
- Plan pricing (premium, deductible, MOOP, copays, coinsurance) — 100% from our MongoDB
planscollection. Audited byte-for-byte against CMS at tier-1 through tier-5. - CSR variants (94/87/73 cost-sharing adjustments) — from
puf.csrVariantson each plan doc. - SBC scenarios (Having a Baby / Diabetes / Simple Fracture cost-share breakdowns) — from
puf.sbcScenarios. - Quality rating (QRS star ratings) — from
puf.qualityRating. - Plan documents (SBC URL, formulary URL, provider directory URL, brochure, enrollment) — from
puf.urls. - Full benefit table (40+ benefits per plan with cost-share) — from
puf.benefitDetails. - Rating areas + age curves — from
premiumsByRatingArea+ageRatesByArea.
Why we still call CMS (the honest answer)
Two reasons across the board, both historical:
Commit
330871eorder of operations moved federal plans from CMS to our MongoDB. Eligibility, drugs, and providers were left on CMS to limit migration scope — plan pricing was the audit-locked part that needed careful proof of byte-for-byte parity with CMS first. Eligibility math is conceptually downstream of pricing math (APTC formula needs SLCSP, which comes from premium data we now own). Drug/provider data was on CMS because the LARK pipeline (ENG-236) hadn't shipped delta-aware refresh yet when those routes were built."CMS as ground truth" psychology — calling CMS feels safer because CMS is the official ACA reference. But our pricing tier audit harness already proves we match CMS byte-for-byte on the data layer; eligibility math is a simpler derivation from the same underlying data.
Migration plans (do NOT execute now — reference only)
These are proposed plans to execute if and only if we hit a real issue: CMS outage during OEP, CMS rate-limit pressure, performance regression, strategic competitor pressure, or compliance / portability requirement. Until then, leaving the CMS calls in place is the right call (not busy work for no needle-moving outcome).
When the trigger fires, file a Linear issue and use the relevant plan below as the starting point.
Migration plan: federal eligibility
Trigger to execute: CMS eligibility endpoint outage / rate-limit / >500ms P95 latency / strategic decision to eliminate CMS dependency.
Scope: Replace src/app/api/eligibility/route.ts federal-state branch (lines 161-207) with an owned calculateFederalEligibility() helper mirroring the existing calculateNyEligibility() in src/lib/owned-plans.ts.
What we already have:
calculateFpl()insrc/lib/csr.ts— FPL % from income + household size.deriveCsrTierFromFpl()insrc/lib/csr.ts— 94/87/73/zero band lookup per 45 CFR 155.305(g).calculateMedicaidThreshold()insrc/lib/csr.ts— currently uniform 138% FPL.findSlcspPremium()insrc/lib/utils.ts— Second Lowest Cost Silver Plan from ourplanscollection.calculateAptc()insrc/lib/utils.ts—max(0, SLCSP - expectedContribution(FPL%) × income).
What we'd need to add:
- State Medicaid expansion table: 10 non-expansion states with different thresholds for parents vs childless adults. KFF publishes this annually. ~100 lines of constants.
- Hardship exemption: narrow edge case, currently not surfaced in our UI — can skip entirely.
- Coverage gap flag: derived from state-expansion-status + FPL band — pure derivation, no new data needed.
Effort: 1-2 days.
- Half day on code (collapse route from 216 → ~120 lines).
- Half day on state Medicaid table + verification.
- Half day on regression test (50-100 scenarios across 30 federal states, byte-for-byte vs CMS output).
Reversibility: Trivial. One file revert.
Acceptance criteria template:
- [ ]
calculateFederalEligibility()returns identical{aptc, csr, is_medicaid_chip, in_coverage_gap}to CMS for 50+ regression scenarios. - [ ] State Medicaid threshold table covers all 10 non-expansion states with parent/childless-adult variants where applicable.
- [ ] Calculator submit + /plans + /plans/[planId] all return correct subsidized prices for a federal Medicaid scenario.
- [ ] Calculator regression diff (
scripts/audit/calculator-baseline-diff.ts) ZERO DIFFS on all 12 scenarios.
Migration plan: drug + provider autocomplete
Trigger to execute: CMS autocomplete rate-limit pressure / want richer name-search / strategic decision.
Scope: Build name-indexed search over formularies_staging + providers_staging, rewrite /api/drugs/autocomplete + /api/providers/autocomplete to query MongoDB first.
What we already have:
formularies_stagingcollection — RxCUI + plan_id + drug_tier + step_therapy + prior_auth + quantity_limit per row. Keyed by RxCUI.providers_stagingcollection — NPI + plan_id + network_tier per row. Keyed by NPI.
What we'd need to add:
- Drug name index: denormalize the RxNorm name (or brand name) into
formularies_stagingand create a MongoDB Atlas Search or$textindex overname. - Provider name index: denormalize the NPPES-derived display name into
providers_stagingand create a similar index. - Result shape parity: ensure the response shape matches what
useDrugAutocomplete()+useDoctorAutocomplete()expect today (no client-side changes).
Effort: 2-3 days (most of it on the index build + verification that names roll up cleanly across issuers).
Reversibility: Trivial. Routes can be A/B'd via env var.
Migration plan: drug + provider coverage (/covered)
Trigger to execute: After autocomplete migration. CMS /covered rate-limit pressure or strategic decision.
Scope: Flip /api/drugs/covered + /api/providers/covered to query our staging tables first, CMS only as fallback for the narrow gap case (or eliminate CMS entirely if our data confidence is high enough).
What we already have:
lookupStagingDrugTiers()+lookupStagingProviderNetworks()— already implemented, currently used as gap-fill.- Both staging tables are production-confidence after LARK pipeline (ENG-236) shipped delta-aware refresh.
What we'd need to verify:
- Coverage of staging tables vs CMS — are there plan + drug combinations where CMS knows the answer but our staging table doesn't?
- Issuer §1311 file completeness — some issuers may file partial data.
Effort: 2 days (most of it on coverage gap audit, not code).
Reversibility: Trivial. Same A/B-via-env-var pattern.
Migration plan: /api/counties CMS fallback removal
Trigger to execute: When zip_county collection is verified to cover 100% of valid US ZIPs (including all SBE-state ZIPs).
Scope: Remove lookupZipViaCms() fallback path from /api/counties. Pure code cleanup.
Effort: 1 hour + ZIP coverage audit (already tracked under tier-1 + tier-1.5 audit harness).
Operational guidance
CMS outage incident response
If CMS is down (outage / 5xx / >5s latency):
- Federal eligibility breaks (all federal calculator submits fail).
- Drug + provider autocomplete breaks (no results return).
- Drug + provider coverage check breaks (coverage filters return empty).
- Plan pricing display continues working (served from our DB).
- NY traffic continues working (no CMS dependency).
- Counties resolution continues working for happy-path ZIPs (CMS fallback used only for non-ingested SBE ZIPs).
Mitigation: until federal eligibility is migrated, there is no full-flow mitigation. NY users are unaffected. Federal users see calculator submit errors. Consider posting a banner: "Federal subsidy calculator temporarily unavailable while CMS systems recover."
When federal eligibility is migrated (per plan above), the only remaining CMS surface is the drug/provider routes. Outage impact narrows to "doctor + drug search temporarily unavailable" — calculator + plan pricing + plan list all keep working.
Route-to-avoid guidance (for future engineers)
When adding a new feature or surface:
- DO NOT add new direct calls to
cms-api.tshelpers (drugsAutocomplete,providersAutocomplete,drugsCovered,providersCovered,fetchEligibility). - DO consume the existing API routes (
/api/drugs/*,/api/providers/*,/api/eligibility) — they're the centralized abstraction. If we ever flip them to DB-first, every caller upgrades for free. - DO prefer our DB collections directly when the data is in
plans,formularies_staging,providers_staging, orzip_county.
What the audit harness covers (and doesn't)
scripts/audit/tier-1throughtier-5— verify our plan pricing data matches CMS byte-for-byte. Does NOT verify eligibility math or coverage data.- If we migrate federal eligibility, we'll need a
tier-6-eligibility.jsaudit harness as the regression gate. - If we migrate drug/provider coverage, we'll need similar tier-N harnesses against CMS.
Cross-references
docs/data-sources/cms-api.md— CMS Marketplace API reference.docs/data-sources/puf-data.md— PUF ingest pipeline (the source of allpuf.*fields).src/lib/owned-plans.ts—OWNED_DATA_STATES,calculateNyEligibility,searchOwnedPlans.src/lib/csr.ts— FPL + CSR tier math.src/lib/utils.ts— SLCSP + APTC calculation.src/lib/fetch-plans.ts— Unified eligibility + plans pipeline used by bothuseCalculator()andPlansMarketplace.- ADR 0005 — Delayed jobs architecture (CMS is not used for delayed jobs).