CMS-0057-F FHIR Prior Auth API for 2027: Keep the LLM Out of Order-Sign
CMS-0057-F makes FHIR prior auth APIs due in 2027. How CRD rides CDS Hooks, why no LLM belongs in order-sign, and where AI can safely draft DTR answers.
Mostly about building products, healthcare tech, and lessons learned along the way.
CMS-0057-F makes FHIR prior auth APIs due in 2027. How CRD rides CDS Hooks, why no LLM belongs in order-sign, and where AI can safely draft DTR answers.
In a prior auth stack, the model drafts text. It never decides coverage, and it never runs inside the order-sign hook.
That matters now because of CMS-0057-F. Impacted payers have to run a FHIR Prior Authorization API, with compliance dates generally beginning January 1, 2027. CMS strongly recommends building it on the Da Vinci CRD, DTR and PAS guides.
I haven't shipped an order-sign hook. Both of our CDS Hooks services, in MedHook and eSpiral, are patient-view only, and we chose that on purpose. Neither one calls an LLM inside the hook. eSpiral's cards come from advisories already in the database, so the hook path is a FHIR read and a lookup. Electronic prior auth (CRD, DTR, PAS) is on our roadmap, but we've deferred it until we have a payer partner. For now, prior auth for our users is a manual submission with a better-drafted letter. Everything below is how I'd build the payer workflow, and our own latency numbers shaped most of it.
From the CMS fact sheet:
Part of the rule is already live. The operational provisions generally began January 1, 2026. Payers now owe a specific reason for every denial, however the request came in. Decisions are due within 72 hours for expedited requests and seven calendar days for standard ones. QHP issuers on the FFEs are exempt from those clocks but not from the APIs. The first annual prior auth metrics were due on payer websites by March 31, 2026.
There's also demand on the provider side. Starting with the CY 2027 performance period, MIPS adds a yes/no attestation. To answer yes, a clinician must have requested at least one prior authorization electronically through a Prior Authorization API using CEHRT data. So EHR vendors and anyone building provider-side tooling have a reason to care too.
CMS requires FHIR R4.0.1, US Core 3.1.1 and SMART App Launch 1.0.0. It recommends CRD STU 2.0.1, DTR STU 2.0.0 and PAS STU 2.0.1. Payers can use newer ONC-approved versions as long as nothing breaks for API users.
The CRD build has moved on to a 2.2.0 snapshot that references CDS Hooks 2026Jan. STU2 references CDS Hooks 2.0. Two of the changes affect your architecture:
My approach: build to STU 2.0.1 because that's what CMS names, and design for 2.2's client behavior. Assume the EHR will stop waiting at the deadline.
CRD STU2 says services SHALL respond within the target time 90% of the time. The target is 5 seconds for most hooks. It rises to 10 seconds for appointment-book, and for order-sign and order-dispatch calls where nothing could have been cached. The primary hooks are appointment-book, order-sign and order-dispatch. For those, you SHALL return at least a Coverage Information system action, even when the answer is "we need more information."
Under STU2, a slow service holds up a clinician who is trying to sign an order. The IG itself says slow services can turn CRD's benefit "into a detriment." Under 2.2, a slow service gets skipped.
The prefetch rules point the same way. Prefetch SHOULD include only data needed on every invocation. Anything needed only some of the time SHALL be fetched by query with the token the EHR hands you, and that token SHOULD expire in 30 seconds or less. Separately, STU2 lists the base data a CRD service needs: Patient, the relevant Coverage, the authoring and requested-performer Practitioner and Organization, the requested Location, and the Medication or Device.
If you haven't wired CDS Hooks into a real EHR before, read what nobody tells you before you build first. The discovery and auth plumbing will eat more of your schedule than the rules engine will.
In MedScrub, a skill run blocks 30 to 50 seconds on the model. That's six to ten times the whole CRD budget, before you've done a single FHIR read.
MedScrub's de-identification NER pass had a 10-millisecond budget. A design partner's diagnostics showed it blowing that budget 549 times and falling back to partial results each time. Each fallback could let PHI through unscrubbed. We fixed it by scaling the budget with text size and always running every pass on the desktop. A dense 40,000-character field now takes about 86 ms.
The lesson for order-sign: you can tune deterministic work until it fits a budget. You can't tune a 30-second model call down to five. And when a budget is blown, the fallback is where the damage happens. In CRD, the fallback is a card that shows up late or never.
Inside the hook, run only deterministic logic over prefetched data. Do anything generative before the clinician gets to the order, never while they're signing it.
// order-sign: no model calls on this path
async function orderSign(req: HookRequest) {
const key = cacheKey(req); // STU2: order ids; 2.2: patient + ordering org
const rules = await rulesCache.get(key) ?? await loadRules(req, { deadlineMs: 3500 });
if (!rules) return coverageInfo({ determination: "unknown", needsDtr: true });
return coverageInfo(evaluate(rules, req.context.draftOrders))
The fallback comes from the IG. If you can determine eligibility in the window but not prior auth, return a card saying the service is covered and DTR is needed. Where the coverage-information code is clinical or admin, STU2 says you SHOULD gather the rest through DTR, and it signals that this will become SHALL later. Map your internal shape to the IG's coverage-information extension at the edge.
Use order-select to warm that cache. In 2019, CRD called order-select "probably the most important" hook. STU2 makes it optional and treats it mainly as a chance to start slow back-end queries early. EHRs can configure it to return no cards at all. Use it to load payer rules and coverage lookups, not to start a model run on the clinician's clock.
Generative work fits upstream of the hook, and it follows the eSpiral pattern: the hook serves text that already exists.
Run the model offline over payer policy documents. Have it draft the card text: what's required, which documentation tends to be missing, which covered alternatives exist. A human reviews the draft and stores it with a version and a link back to the source policy. At hook time, you look it up. The model wrote those words last week, and the hook only reads them.
The actual determination (covered or not, PA needed or not) comes from encoded rules, never from the summary. The 2.2 snapshot adds an accuracy requirement: CRD guidance like "covered" or "PA not needed" SHALL be as accurate as what the payer says through its portal or phone line. A model that paraphrases a policy into "no PA required" is a compliance problem.
DTR hands the EHR the payer's questionnaire and prefills what it can from the chart. The structured fields are the easy part. The hard part is the answers buried in notes, attachments and forms that were never meant to be structured. That's where a model helps.
We've done this kind of extraction. On one integration, data came from Healthie as HTML forms, and we used LLMs to pull it out and move it into FHIR. The closest real analogue to a field the chart can't cleanly support came from MedHook's HL7 appointment mapping. Legacy Rhapsody feeds send date-times with no UTC offset, and there's no safe way to guess the zone. We made the transform fail the step by default instead of guessing, with a warn-mode for teams migrating old behavior.
Our chart-reading templates follow the same rule: "answer ONLY from the record; when a requested item is not in the chart, say so explicitly rather than guessing." Blank or ambiguous values get flagged, never filled. For a DTR questionnaire, that means every drafted answer carries its sources, and an answer with no source isn't a draft:
[
{ "linkId": "prior-pt-duration", "draft": "6 weeks, completed",
"sources": ["DocumentReference/8812", "Encounter/4410"], "status": "drafted" },
{ "linkId": "bmi", "draft": null, "sources": [], "status": "not-in-chart" }
]
Check the sources in code, not just in the prompt. Reject any draft that cites a resource that doesn't exist or wasn't in the context you gave the model. The clinician reviews, and only reviewed answers go into the QuestionnaireResponse. I'd rather hand a clinician three honest blanks than one confident wrong answer.
Our prior-auth AI work today is an appeal-letter drafter in MedScrub. It reads the Coverage, Claim and ClaimResponse records and the chart, then drafts the medical-necessity argument with the evidence laid out. The prompt rule is blunt: "Do NOT fabricate clinical data. Only cite evidence present in the patient record. If evidence is limited, note what additional documentation could strengthen the appeal." The clinician reviews and sends it. Nothing goes to a payer automatically.
My hard line: we will never have a model say whether something should be covered. I haven't seen ours try. I think part of the reason is that the skills are written as advocacy, not adjudication. A prompt that asks "does this record meet the criteria?" is asking the model to adjudicate, whatever you call it. A prompt that asks "build the strongest honest case from this record, and list what's missing" can't produce a coverage decision, because nobody asked for one.
CMS-0057-F helps here too. Since 2026, every denial has to come with a specific reason. For an appeal drafter, that reason is the target. It gives the model a specific claim to answer instead of a vague "not medically necessary."
For the regulatory case on why clinicians must be able to review what the model produced, see my take on FDA's 2026 CDS guidance. In practice: do generative work offline (card text) or as reviewable drafts (DTR answers, appeal letters), and keep order-sign deterministic.