Upload a Receipt

Send an itemized receipt so Sail can split a mixed-basket expense into items and classify each one.

Some expenses can't be classified from the transaction alone. A $49.00 charge at a store that sells both prescriptions and birthday cards gets itemization_required, because Sail can't tell what was in the basket. An itemized receipt supplies that detail. This guide shows how to upload a receipt, by your backend or by your end user, and how to read the result.

Sail reads the receipt, creates one expense per item, and classifies each item on its own. See Expense Classification for what each outcome means.

Requirements

Before you start, make sure you have:

  • A Sail user created for your end user.
  • An API key with the expenses scope.
  • A webhook endpoint configured to receive events.
  • An itemized receipt as an image or a PDF.

Step 1: Choose who uploads the receipt

You can upload the receipt from your backend, or let your end user upload it in Sail's Upload Receipt Widget. Both paths reclassify the expense the same way.

PathUse it whenHow
Your backendYou already collect receipts in your own appUpload Receipt
Your end user, in the Upload Receipt WidgetYou want Sail to show the upload screenCreate Hosted Session with flow set to upload_receipt

Your backend can also upload a receipt that has no expense yet. Sail then creates new expenses from the receipt instead of updating an existing one.

Step 2: Upload the receipt

Follow the path you chose in Step 1.

Upload from your backend

To attach the receipt to an expense, call Upload Receipt with the expense id in the path and the file in a file field. The request uses multipart/form-data.

curl -X POST https://live.savewithsail.com/api/v1/users/usr_abc123/expenses/exp_9f2c/receipt \
  -H "Authorization: Bearer sk_live_..." \
  -F "[email protected]"

The response returns 202 with a job_id. Sail processes the receipt asynchronously and sends the result by webhook in Step 3.

To upload a receipt that has no expense yet, call Upload Receipt (no expense) at POST /users/USER_ID/expenses/receipt, with the same file field. Sail reads the receipt and creates a new expense from it.

Let your end user upload in the Upload Receipt Widget

To let your end user upload the receipt, create a hosted session with flow set to upload_receipt. Sail opens the Upload Receipt Widget. Pass the expense_id in options to attach the receipt to that expense.

curl -X POST https://live.savewithsail.com/api/v1/hosted-sessions \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "usr_abc123",
    "flow": "upload_receipt",
    "options": {
      "expense_id": "exp_9f2c"
    }
  }'

Open the url in an iframe or redirect your end user to it. The session works once and stops working at expires_at. See Connect an Account for how hosted sessions and the two widgets relate.

Step 3: Read the result

Sail reports the receipt's progress by webhook, so there's nothing to poll. Expect these events:

EventWhat it means
receipt.uploadedSail received the receipt.
receipt.processedSail itemized and classified the receipt. expense_ids lists the resulting expenses.
receipt.failedSail couldn't read the receipt. reason says why, for example unreadable.
📘

Check the Sail-Signature header before you process any event. Delivery is at-least-once, so deduplicate on the event id. See Receiving New Data for both.

When you receive receipt.processed, fetch the expenses in expense_ids with Get Expense, or list them with List Expenses. Each item from the receipt is its own expense, with its own section_213d_classification. When the receipt was attached to an existing expense, each new item carries a parent_expense_id that points back to it.

If you receive receipt.failed, ask your end user for a clearer image and upload again.

Next steps


Did this page help you?