Automate Denial Appeals
When a prior authorization is denied, your RCM platform needs to generate an appeal letter fast. This guide walks through the full denial-to-appeal flow: understanding denial patterns, preparing a de-identified clinical summary, generating the letter, and deciding which appeals to pursue.
Estimated integration time: 30 minutes. Use a sandbox key (tln_test_) for development -- it returns realistic sample letters.
Look up denial patterns for context
When a denial comes in, start by fetching the payer's rules for the denied procedure. The common_denial_reasons field tells you what typically triggers denials, which helps you understand whether this denial fits a known pattern or is unusual.
curl -H "X-API-Key: tln_test_YOUR_KEY" \
"https://talonapi.dev/api/v1/rules?payer=unitedhealthcare&cpt=27447"{
"common_denial_reasons": [
{
"reason": "Insufficient documentation of conservative treatment"
},
{
"reason": "Medical necessity not established"
},
{
"reason": "Missing functional assessment scores"
}
],
"approval_rate": 0.78
}Match the denial reason from the payer's letter to one of these patterns. If it matches a known pattern, the appeal generator can craft more targeted arguments.
Prepare a de-identified clinical summary
clinical_summary field must be de-identified before submission. Requests containing patient names, dates of birth, Social Security numbers, or member IDs will receive a 422 phi_detected error.Strip all 18 HIPAA identifiers from the clinical summary. Use age ranges or approximate ages, and refer to the patient generically. Here is what a properly de-identified summary looks like:
"Patient is 67yo female with severe bilateral knee OA,
Kellgren-Lawrence Grade IV. BMI 31.2. Failed 6 months of
physical therapy (3x/week), NSAIDs (naproxen, meloxicam),
and bilateral corticosteroid injections (3 rounds). WOMAC
pain score 72/100. Functional limitation: unable to climb
stairs or walk more than one block without significant pain.""Jane Smith (DOB 03/15/1959, Member ID AET-882991) presents
with bilateral knee OA..."Your integration should strip PHI programmatically before calling the API. If a phi_detected error is returned, do not retry with the same text -- strip the identifiers first.
Generate the appeal letter
Post the denial details and de-identified summary to POST /api/v1/appeals/generate. The API uses AI to generate a clinically appropriate appeal letter backed by the payer's own denial pattern data.
curl -X POST "https://talonapi.dev/api/v1/appeals/generate" \
-H "X-API-Key: tln_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"payer": "UnitedHealthcare",
"cpt_code": "27447",
"denial_reason": "Insufficient documentation of conservative treatment",
"denial_code": "J1",
"clinical_summary": "Patient is 67yo female with severe bilateral knee OA, Kellgren-Lawrence Grade IV. BMI 31.2. Failed 6 months PT (3x/week), NSAIDs (naproxen, meloxicam), and bilateral corticosteroid injections (3 rounds). WOMAC pain score 72/100. Unable to climb stairs or walk >1 block."
}'Parse the appeal response
The response contains the full letter and metadata to help you decide whether to pursue the appeal:
{
"appeal_letter": "Dear Medical Director,\n\nI am writing to appeal the denial of prior authorization for CPT 27447 (Total Knee Arthroplasty) for this patient. The denial was issued under reason code J1, citing insufficient documentation of conservative treatment. I respectfully disagree with this determination and provide the following evidence:\n\n1. CONSERVATIVE TREATMENT HISTORY\nThe patient has undergone an extensive and well-documented course of conservative management over the past 6 months, including:\n- Physical therapy 3 times per week for 6 months\n- NSAID therapy with naproxen and meloxicam\n- Three rounds of bilateral corticosteroid injections\n\nAll conservative measures have failed to provide adequate relief.\n\n2. CLINICAL SEVERITY\n- Kellgren-Lawrence Grade IV bilateral knee OA on imaging\n- WOMAC pain score of 72/100, indicating severe functional impairment\n- BMI of 31.2 (documented as required)\n- Unable to climb stairs or walk more than one block\n\n3. MEDICAL NECESSITY\nGiven the failure of 6 months of multimodal conservative treatment and the severity of radiographic and functional findings, total knee arthroplasty meets the criteria for medical necessity under UnitedHealthcare's own clinical policy guidelines.\n\nI request that this denial be overturned and authorization granted for the requested procedure.\n\nSincerely,\n[Physician Name, MD]",
"key_arguments": [
"6 months documented PT (exceeds typical 6-week minimum)",
"Failed multiple NSAID classes",
"3 rounds corticosteroid injections without relief",
"Kellgren-Lawrence Grade IV (most severe)",
"WOMAC 72/100 (severe functional limitation)"
],
"success_probability": 0.72
}appeal_letter -- The full letter text, ready for physician review and signature. Always includes placeholders like [Physician Name, MD] that need to be filled in.
key_arguments -- Bullet points summarizing the strongest arguments in the letter. Useful for a quick review UI.
success_probability -- Estimated likelihood that this appeal will succeed, based on historical outcomes data for this payer, procedure, and denial reason combination.
Complete TypeScript integration
A production-ready function that handles the full denial-to-appeal flow:
const TALON_BASE = "https://talonapi.dev";
interface DenialInput {
payer: string;
cptCode: string;
denialReason: string;
denialCode?: string;
clinicalSummary: string;
}
interface AppealResult {
letter: string;
keyArguments: string[];
successProbability: number;
worthPursuing: boolean;
}
export async function generateAppeal(
apiKey: string,
denial: DenialInput
): Promise<AppealResult> {
const headers = {
"X-API-Key": apiKey,
"Content-Type": "application/json",
};
// Generate the appeal letter
const appealRes = await fetch(`${TALON_BASE}/api/v1/appeals/generate`, {
method: "POST",
headers,
body: JSON.stringify({
payer: denial.payer,
cpt_code: denial.cptCode,
denial_reason: denial.denialReason,
denial_code: denial.denialCode,
clinical_summary: denial.clinicalSummary,
}),
});
if (!appealRes.ok) {
const err = await appealRes.json();
if (err.error?.code === "phi_detected") {
throw new Error(
"Clinical summary contains PHI. Remove patient names, DOBs, SSNs, and member IDs before retrying."
);
}
throw new Error(`Appeal generation failed: ${err.error?.message ?? appealRes.statusText}`);
}
const data = await appealRes.json();
// Decide if the appeal is worth pursuing
// Threshold is configurable -- adjust based on your org's cost/benefit analysis
const PURSUIT_THRESHOLD = 0.4;
const worthPursuing = data.success_probability >= PURSUIT_THRESHOLD;
return {
letter: data.appeal_letter,
keyArguments: data.key_arguments,
successProbability: data.success_probability,
worthPursuing,
};
}
// Usage
try {
const result = await generateAppeal("tln_test_abc123", {
payer: "UnitedHealthcare",
cptCode: "27447",
denialReason: "Insufficient documentation of conservative treatment",
denialCode: "J1",
clinicalSummary:
"Patient is 67yo female with severe bilateral knee OA, " +
"Kellgren-Lawrence Grade IV. Failed 6 months PT, NSAIDs, " +
"and corticosteroid injections. WOMAC 72/100.",
});
if (result.worthPursuing) {
console.log(`Appeal recommended (\${(result.successProbability * 100).toFixed(0)}% success rate)`);
console.log("Key arguments:", result.keyArguments);
// Queue letter for physician review
} else {
console.log("Low success probability -- consider alternative strategies.");
}
} catch (err) {
if (err instanceof Error && err.message.includes("PHI")) {
// Route to PHI scrubbing workflow
console.error(err.message);
}
}Python equivalent
import requests
from dataclasses import dataclass
TALON_BASE = "https://talonapi.dev"
PURSUIT_THRESHOLD = 0.4
@dataclass
class AppealResult:
letter: str
key_arguments: list[str]
success_probability: float
worth_pursuing: bool
class PHIDetectedError(Exception):
pass
def generate_appeal(
api_key: str,
payer: str,
cpt_code: str,
denial_reason: str,
clinical_summary: str,
denial_code: str | None = None,
) -> AppealResult:
headers = {"X-API-Key": api_key}
# Generate the appeal letter
payload = {
"payer": payer,
"cpt_code": cpt_code,
"denial_reason": denial_reason,
"clinical_summary": clinical_summary,
}
if denial_code:
payload["denial_code"] = denial_code
appeal_res = requests.post(
f"{TALON_BASE}/api/v1/appeals/generate",
headers={**headers, "Content-Type": "application/json"},
json=payload,
)
if appeal_res.status_code == 422:
err = appeal_res.json()
if err.get("error", {}).get("code") == "phi_detected":
raise PHIDetectedError(
"Clinical summary contains PHI. Remove identifiers before retrying."
)
appeal_res.raise_for_status()
data = appeal_res.json()
return AppealResult(
letter=data["appeal_letter"],
key_arguments=data["key_arguments"],
success_probability=data["success_probability"],
worth_pursuing=data["success_probability"] >= PURSUIT_THRESHOLD,
)
# Usage
try:
result = generate_appeal(
api_key="tln_test_abc123",
payer="UnitedHealthcare",
cpt_code="27447",
denial_reason="Insufficient documentation of conservative treatment",
denial_code="J1",
clinical_summary=(
"Patient is 67yo female with severe bilateral knee OA, "
"Kellgren-Lawrence Grade IV. Failed 6 months PT, NSAIDs, "
"and corticosteroid injections. WOMAC 72/100."
),
)
if result.worth_pursuing:
prob = round(result.success_probability * 100)
print(f"Appeal recommended ({prob}% success rate)")
print("Key arguments:", result.key_arguments)
else:
print("Low success probability -- consider alternatives.")
except PHIDetectedError as e:
print(f"PHI error: {e}")
# Route to PHI scrubbing workflowTips for production
success_probability descending and work the high-probability appeals first. Appeals with under 30% probability may not be worth the staff time.422 phi_detected, route the case to your de-identification workflow rather than failing silently. Common culprits: patient names in clinical notes, embedded DOBs, and member IDs in assessment headers.success_probability may be based on limited historical data. Flag these cases for extra physician attention. denial_code parameter is optional but significantly improves letter quality. The AI uses it to match against known denial patterns and craft more targeted arguments.