Extracting Structured Fields From a Warranty Claim Denial Letter
9 min read · updated August 11, 2026
Two fields on a denial letter are worth extracting: the reason and the time limit. Both are traps. The reason is a reference into a document you do not have, and the time limit is a computation that the letter gives you the inputs for and not the answer to.
What the letter carries
A claim denial is a short letter with an unusually high density of dates and identifiers, most of which look alike:
- Claim number, policy or contract number, and product identifiers — model, serial, sometimes a service request number issued by a third-party administrator that is different again.
- Five or six dates. Date of purchase, date coverage began, date coverage ends, date of the reported failure, date the claim was filed, date of inspection, date of the letter. A schema with a field called
dateis guaranteed to be wrong; each of these needs its own name. - The decision — denied, denied in part, approved with conditions, or deferred pending information.
- Cited exclusions, as a list.
- The appeal or reconsideration provision: a window, a method, an address, and often a list of what must accompany an appeal.
- Boilerplate — reservation of rights, a statement that the letter is not a waiver, contact details for a state regulator. Prose that a summarising model will happily blend into the decision if you ask it for “the outcome” in one free-text field.
A cited exclusion is a pointer
Denials cite by clause: “coverage is excluded under Section 4(b)(iii) of your agreement”. On its own, that string is close to worthless in a structured record. Section 4(b)(iii) of which revision? Warranty terms are versioned, contracts issued a year apart renumber, and the same product line may be sold under three different forms in the same year.
So capture three things where most schemas capture one:
{
"cited_exclusions": [
{
"clause_ref": "4(b)(iii)",
"clause_text_quoted": "damage arising from improper installation by a party
other than an authorized installer",
"document_reference": { "form": "HWP-2023-04", "version_date": "2023-04-01" }
}
]
}The quoted text is the field that survives. Letters usually reproduce the clause they are relying on, and that quotation is self-contained in a way the reference number is not — it can be matched against a clause library, categorised, and counted across claims without ever resolving the version. Extract the reference too, because when it does resolve it is exact, but do not build the downstream logic on it alone.
Note the list. A denial resting on two independent exclusions is ordinary, and a single reason string will capture the first and drop the second — which matters, because an appeal that defeats one exclusion changes nothing if a second still stands. This is the general case for arrays over scalars whenever a document can repeat a thing, and it is the same reasoning as designing a schema before you have seen every variant: the cost of a list when there is one item is nothing, and the cost of a scalar when there are two is a silent loss.
The deadline is not a date
The tempting field is appeal_deadline: "2026-04-11". Do not extract it, because the letter does not contain it. The letter contains a window, and turning a window into a date needs four inputs plus a calendar:
- The trigger. “from the date of this letter”, “from the date of receipt”, “from the date of the denial decision”. These are different days, and the receipt one is not knowable from the document at all.
- The count. Usually printed twice, in words and figures — “thirty (30)” — which is a free consistency check worth performing, because a mismatch between the two is a real drafting defect that occurs.
- The unit. Days or months.
- The basis. Calendar days or business days. Frequently unstated, in which case the correct extraction is null and not a guess.
Add to that a jurisdiction, because business days require a holiday calendar and holiday calendars are local. None of that belongs inside a vision model. Emit the components; compute downstream, where the calendar lives and where the computation can be re-run when somebody notices the holiday table was wrong.
{
"appeal_window": {
"count": 30,
"count_in_words": "thirty",
"unit": "days",
"basis": null,
"trigger": "letter_date",
"quote": "You may request reconsideration within thirty (30) days of the date
of this letter."
},
"letter_date": "2026-03-12"
}The same window, two answers
Take that letter, dated Thursday 12 March 2026, with a thirty-day window. If the basis is calendar days:
12 March 2026 + 30 calendar days March has 31 days, so 19 days remain in March after the 12th 30 - 19 = 11 days into April = Saturday 11 April 2026 rolled to the next business day = Monday 13 April 2026
If the basis is business days, counting weekdays and ignoring public holidays:
Fri 13 Mar = 1 Mon 23 Mar = 7 Mon 30 Mar = 12 Mon 6 Apr = 17 Mon 16 Mar = 2 Tue 24 Mar = 8 Tue 31 Mar = 13 Tue 7 Apr = 18 Tue 17 Mar = 3 Wed 25 Mar = 9 Wed 1 Apr = 14 Wed 8 Apr = 19 Wed 18 Mar = 4 Thu 26 Mar = 10 Thu 2 Apr = 15 Thu 9 Apr = 20 Thu 19 Mar = 5 Fri 27 Mar = 11 Fri 3 Apr = 16 Fri 20 Mar = 6 = Thursday 9 April 2026
Two readings of one sentence, two deadlines four days apart — and the business-day answer is not final either, because Good Friday falls on 3 April in 2026. If the applicable calendar treats it as a holiday, day 16 moves and the deadline becomes Friday 10 April. A model asked for “the appeal deadline” will return one date, with no indication that it chose a basis and a holiday policy on your behalf.
The same pattern applies to every relative date in every document, and it is worth generalising: extract the expression, not the evaluation. A validation rule for a date field can then check the derived date for impossibility separately, which is a different job from producing it.
Letters that deny in part
The last structural trap is the letter that is not a denial. Warranty administrators routinely approve part of a claim and deny the rest: “the compressor is covered; the resulting water damage to flooring is excluded under Section 7(a)”. A schema with a top-level decision enum forces that into one value, and whichever value it picks is half wrong.
Model the decision per claimed item:
{
"overall_decision": "partial",
"items": [
{ "description": "compressor replacement", "decision": "approved",
"amount_allowed": "1420.00", "cited_exclusions": [] },
{ "description": "flooring water damage", "decision": "denied",
"amount_allowed": "0.00", "cited_exclusions": ["7(a)"] }
],
"amount_claimed": "6180.00",
"amount_allowed_total": "1420.00"
}Two checks then become available for free. The item amounts must sum to the stated allowed total, which catches a dropped item on a letter that lists six. And an item marked denied with no cited exclusion is a suspicious record — either the letter genuinely gave no reason, which is itself worth surfacing, or the exclusion was missed. Route those to review rather than accepting them, using the same threshold-and-route mechanics as any other uncertain field.
One practical note about the pages themselves: these letters are almost always generated, printed, signed and scanned, so the copy you receive is an image with a letterhead band across the top of every page and a signature block that OCR reads as noise. The appeal instructions are very often on the reverse or on a second page that looks like a form rather than a letter. A pipeline that takes only page one gets the decision and loses the window, which is the field with a clock on it.