Expense Classification

Learn how Sail classifies transaction-level and item-level expenses, and how to use the results.

Before a user can be paid back from their HSA/FSA, Sail has to decide that what they bought actually qualifies. That decision is expense classification, and eligible is the outcome a reimbursement needs.

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. It can be eligible or ineligible, or it can need itemization or a Letter of Medical Necessity (LMN). 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 the process of checking an expense against a defined set of eligibility rules to determine whether it qualifies as a medical expense that a benefit account can pay for.

Today, Sail classifies against Section 213(d) of the US tax code and IRS Publication 502, the same rules HSA, FSA, and HRA administrators use to decide what counts as a qualified medical expense. The result lives on the Expense 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 classification outcomes

section_213d_classification.status surfaces one of four outcomes:

Expense classification outcomes: eligible, ineligible, needs itemization, and needs a Letter of Medical Necessity, with the next step for each.
📘

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

When an expense needs itemization

itemization_required applies to transaction-origin expenses, meaning origin.type: transaction. It means the purchase came from a mixed-basket store (one 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 for the difference between transaction- and item-level data.

When an expense needs a Letter of Medical Necessity

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.

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 on an external connection. Instead of calling a separate "classify this" endpoint, classification happens as part of processing the expense itself.

How classification results reach you

Classification results arrive by webhook, not by polling. See Webhook Events for the full payload shape of each one.

Two webhooks tell you that results are ready:

  • For any expense: expenses.213d_verified fires once every expense on the user has finished classification. Sail sends it only if expense notifications are enabled on your partner account.
  • For a pushed batch: transactions.ingested also fires at the job level once the whole batch finishes, listing expense_ids.

Once notified, the result is available from Get Expense or List Expenses. See Receiving New Data for how to verify and deduplicate any webhook delivery, classification included.

Resolving itemization_required

There are two ways to supply the missing detail:

  • Attach an itemized receipt through Upload Receipt or the Upload Receipt Widget, the upload_receipt flow of a hosted session. 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.
  • Connect the store itself. When merchant.supported_merchant_id is non-null, Sail has a connector for that store, and prompting the user to log into it supplies the item-level detail for the expenses already waiting on it, as well as for their future purchases there. See Classifying Item-Level Expenses for how item-level data changes which outcomes you see.

Next steps


Did this page help you?