Create and Track a Reimbursement

Learn how to reimburse a user from their own HSA or FSA for eligible expenses, and track the payout to completion.

Once a user's expenses are eligible, Sail can pay them back from their own HSA or FSA. A reimbursement draws directly from the user's benefit_account connection. Sail doesn't fund the payout, and neither does your application.

In this guide, you'll confirm the expenses are eligible and the account balance covers them, create a reimbursement, and track it to completion.

Requirements

Before you start, make sure you have:

  • A Sail user created for your end user, with at least one benefit_account connection and one eligible expense already in Sail.
  • An API key with the expenses and benefit_account scopes.
  • A webhook endpoint configured to receive events, if you plan to track status by webhook instead of polling.

Where the eligible expenses come from

A reimbursement pays back purchases your end user already made, so those purchases need to be in Sail as expenses first. Most users pay with a regular credit or debit card, so the usual way is to connect that card through the Connect Account Widget with connection_type set to card. Sail then brings in the card's transactions as expenses and classifies each one. Expenses from a store connection, a receipt upload, or transactions you push yourself work the same way.

⚠️

Creating a reimbursement starts a real payout from your end user's HSA or FSA. You can't test a reimbursement or cancel one after it starts. Confirm the expenses and the amount before you call Create Reimbursement.

Step 1: Confirm the expenses are eligible

A reimbursement can only include expenses whose Section 213(d) classification has already resolved to eligible. Call List Expenses with status set to eligible to confirm which ones qualify.

curl "https://live.savewithsail.com/api/v1/users/usr_abc123/expenses?status=eligible" \
  -H "Authorization: Bearer sk_live_..."

Note the id and amount of each expense you plan to include. You need the ids for the reimbursement request in Step 3, and the amounts to check against the account's balance in the next step.

Step 2: Confirm the account balance covers the total

The funding account's cash balance must cover the sum of the expenses you're reimbursing, checked against that total, not against each expense individually. Call Get Account and compare its cash balance to the sum you noted in Step 1.

curl https://live.savewithsail.com/api/v1/users/usr_abc123/accounts/acct_77b1 \
  -H "Authorization: Bearer sk_live_..."

As the response in Step 1 showed, the two eligible expenses are 27.02 and 42.98, for a total of 70.00. That total is well under the 1050.50 cash balance returned above, so the reimbursement can proceed. See Account Connections for how benefit_account connections and their balances work.

Step 3: Create the reimbursement

With both preconditions met, create the reimbursement by calling Create Reimbursement. The request identifies the user by user_id in the path, and takes the expense_ids to reimburse and the funding account_id in the body.

curl -X POST https://live.savewithsail.com/api/v1/users/usr_abc123/reimbursements \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "expense_ids": ["exp_9f2c", "exp_d92a"],
    "account_id": "acct_77b1"
  }'

Save the id (reimb_8c1f in this example). Sail immediately begins contacting the account's administrator, so you don't call a separate endpoint to start processing.

An expense that isn't eligible, or that's already part of another in-flight reimbursement, returns a 409 conflict instead of a new reimbursement.

Step 4: Track the reimbursement to completion

A reimbursement moves through pending, submitted, processing, and completed on its way to a successful payout, or to failed if something goes wrong. See Reimbursement for what each status means and the two distinct failure causes.

You can track the transitions in either of two ways:

  • The first is to listen for the reimbursement.status_changed webhook, which Sail sends on every transition:
{
  "id": "evt_7f3k9m",
  "event": "reimbursement.status_changed",
  "created_at": "2026-08-12T09:15:00Z",
  "user_id": "usr_abc123",
  "data": {
    "reimbursement_id": "reimb_8c1f",
    "previous_status": "submitted",
    "status": "processing",
    "failure_reason": null
  }
}
  • The second is to poll Get Reimbursement on any cadence and read the status field yourself:
curl https://live.savewithsail.com/api/v1/users/usr_abc123/reimbursements/reimb_8c1f \
  -H "Authorization: Bearer sk_live_..."

Once status reaches completed, the included expenses move to a reimbursed status and receipt_url is populated. See Receiving New Data for how to verify and deduplicate any webhook delivery, reimbursement.status_changed included.

📘

If status becomes failed

Check the failure_reason on the same webhook or on Get Reimbursement. See Reimbursement for the two distinct causes, failed to submit versus denied after submission, and how to read a failure reason.


Did this page help you?