Create and Track a Reimbursement
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 internal user, with at least one
benefit_accountconnection and oneeligibleexpense already in Sail - An API key with the
expensesandbenefit_accountscopes - A webhook endpoint configured to receive events, if you plan to track status by webhook instead of polling
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_..."{
"data": [
{
"id": "exp_9f2c",
"name": "Walgreens",
"amount": 27.02,
"date": "2026-08-05",
"status": "eligible"
},
{
"id": "exp_d92a",
"name": "CVS Pharmacy",
"amount": 42.98,
"date": "2026-08-06",
"status": "eligible"
}
],
"pagination": {
"limit": 25,
"offset": 0,
"total": 2
}
}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_..."{
"id": "acct_77b1",
"type": "hsa",
"balance": {
"cash": 1050.50,
"invested": 500.00
}
}27.02 + 42.98 = 70.00, well under this account’s 1050.50 cash balance (1050.50 means $1,050.50), 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 with the expense ids to reimburse and the funding account id.
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"
}'{
"id": "reimb_8c1f",
"status": "pending",
"amount": 70.00,
"account_id": "acct_77b1",
"expenses": [
{ "id": "exp_9f2c", "name": "Walgreens", "amount": 27.02, "date": "2026-08-05" },
{ "id": "exp_d92a", "name": "CVS Pharmacy", "amount": 42.98, "date": "2026-08-06" }
],
"receipt_url": null
}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. Track the transition either by listening for the reimbursement.status_changed webhook, or by polling Get Reimbursement.
{
"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
}
}curl https://live.savewithsail.com/api/v1/users/usr_abc123/reimbursements/reimb_8c1f \
-H "Authorization: Bearer sk_live_..."{
"id": "reimb_8c1f",
"status": "completed",
"amount": 70.00,
"account_id": "acct_77b1",
"expenses": [
{ "id": "exp_9f2c", "name": "Walgreens", "amount": 27.02, "date": "2026-08-05" },
{ "id": "exp_d92a", "name": "CVS Pharmacy", "amount": 42.98, "date": "2026-08-06" }
],
"receipt_url": "https://<RECEIPT_URL>",
"submitted_at": "2026-08-10T16:02:00Z",
"completed_at": "2026-08-12T09:15:00Z"
}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.