# Expense Classification

Sail ingests transaction-level and item-level expenses and classifies each one, with fine-grained detail, against a rules engine. Every expense ends up in one of four outcomes, eligible, ineligible, needs itemization, or needs a letter of medical necessity. This page explains what each outcome means, what triggers classification, and how you find out once it finishes.

## What expense classification is

Classification, called "adjudication" in Sail's API and webhooks, is how Sail decides whether an expense is a permitted expense under a specific set of rules.

{/* PENDING: Danny asked Jonah (Slack, 2026-08-19) whether the API schema needs updating too, or if "classification" is a docs-only terminology change. Unanswered as of this comment. If the schema also drops "adjudication," this sentence is wrong and should be removed entirely, there'd be nothing left to explain. Every other "adjudicate/adjudication" mention on this page and elsewhere has already been swapped to "classify/classification" since that part is confirmed regardless (Jonah: "I agree!"). */}

Today, Sail classifies against [Section 213(d)](https://www.law.cornell.edu/uscode/text/26/213) of the US tax code and [IRS Publication 502](https://www.irs.gov/publications/p502), the same rules HSA, FSA, and HRA administrators use to decide what counts as a qualified medical expense. The result lives on the [`Expense`](/reference/expenses/get-users-user-id-expenses) object's `section_213d_classification` field, the field your integration checks for the outcome.

However, other rules can be set up for your account on request, beyond the Section 213(d) default rule set. Classification runs automatically on every expense, whether it comes from a synced card, bank, or store connection, or from a batch of transactions you push yourself through an external connection.

## The four outcomes

`section_213d_classification.status` surfaces one of four outcomes:

* **`eligible`**: the expense qualifies as a Section 213(d) medical expense.
* **`ineligible`**: the expense doesn't qualify.
* **`itemization_required`**: Sail can't classify the expense yet, it needs itemization. See below.
* **`lmn_required`**: Sail can't classify the expense yet, it needs a Letter of Medical Necessity. See below.

{/* PENDING: Jonah confirmed (Slack, 2026-08-19) that Expense.status, the top-level field this section used to be built around, is being removed with nothing replacing it ("No"), so section_213d_classification.status is now written up as the sole source of truth above. Still unconfirmed: whether this removal is already live or still upcoming relative to the V0 deadline (2026-08-21). If it turns out the top-level field is still live today, this section describes the post-removal shape early — flag to Danny/Jonah if that timing matters before shipping. See [[08-expense-status-removal]]. */}

<Callout type="info" title="Reimbursement">

Once an expense is `eligible`, it can be reimbursed from the user's HSA/FSA. See [Reimbursement](/docs/concepts/reimbursement) for the preconditions, the status lifecycle, and how a reimbursement can fail.

</Callout>

### Needs itemization

`itemization_required` applies to transaction-origin expenses, meaning `origin.type: transaction`. It means the purchase came from a mixed-basket merchant (a store like Walgreens or Target that sells both eligible and ineligible items in the same transaction), so Sail can't tell what was actually bought from the transaction alone. See [Transaction-Level and Item-Level Expenses](/docs/concepts/transaction-level-and-item-level-expenses) for the difference between transaction- and item-level data.

### Needs letter

`lmn_required` applies to product-origin expenses, meaning `origin.type: product`. It means the item is dual-purpose (something with both a medical and a non-medical use, like a general-purpose humidifier), and it becomes eligible only with a Letter of Medical Necessity from the user's provider.

{/* PENDING: the spec documents a resolution path for itemization_required (Upload Receipt / upload_receipt hosted session) but no equivalent endpoint or flow for resolving lmn_required. This may just be undocumented rather than nonexistent. Confirm with Jonah before asserting either way to a reader, and before Queen's guide assumes a resolution flow exists. */}

## What triggers classification

Classification runs automatically as soon as expense data lands in Sail, regardless of source. That includes expenses synced from a card, bank, or store connection, and expenses you push yourself through [Push Transactions](/reference/connections/post-users-user-id-connections-connection-id-transactions) on an external connection. You never call a separate "classify this" endpoint, classification happens as part of processing the expense itself.

## How you find out the result

Classification results arrive by webhook, not by polling. What you get depends on how the expense entered Sail.

* From a synced connection: `expense.created` for a new, already-classified expense, or `expense.updated` with `section_213d_classification` listed in `changes` if an existing expense's classification changes.
* From a pushed batch: `transactions.ingested` fires at the job level once the whole batch finishes, listing `expense_ids`. Sail also fires `expense.created` per new expense within that batch, the same as a synced connection.

Once notified, the result is available from [Get Expense](/reference/expenses/get-users-user-id-expenses-expense-id) or [List Expenses](/reference/expenses/get-users-user-id-expenses). See [Receiving New Data](/docs/concepts/receiving-new-data) for how to verify and deduplicate any webhook delivery, classification included.

## Resolving needs itemization or needs letter

For `itemization_required`, attach an itemized receipt through [Upload Receipt](/reference/expenses/post-users-user-id-expenses-expense-id-receipt) or an `upload_receipt` flow of the [Connect Account Widget](/docs/concepts/connect-widget). Either path triggers asynchronous reclassification. The outcome arrives as a `receipt.processed` webhook, listing the resulting child expenses, or `receipt.failed` if the receipt is unreadable.

## Next steps

* To push your own transaction data and retrieve eligibility results, see [Classify Your Own Transactions](/docs/guides/classify-your-own-transactions).
* To see how expense data enters Sail in the first place, see [Transaction-Level and Item-Level Expenses](/docs/concepts/transaction-level-and-item-level-expenses).