Receipt Upload

Expense capture: snap or drop a receipt, fill in the details, or let your OCR do it, and submit.

"use client"

import { toast } from "sonner"

import { ReceiptUpload } from "@/components/receipt-upload"

/** Stand-in for your OCR or AI extraction. */
async function extract() {
  await new Promise((resolve) => setTimeout(resolve, 900))
  return { merchant: "Blue Bottle Coffee", amount: "14.50", category: "Meals" }
}

export default function ReceiptUploadExample() {
  return (
    <div className="w-full max-w-xl">
      <ReceiptUpload
        extract={extract}
        onSubmit={(expense) =>
          toast.success("Expense submitted", {
            description: `${expense.merchant} · ${expense.amount}`,
          })
        }
      />
    </div>
  )
}

Installation

npx shadcn@latest add @uploadcn/receipt-upload

Usage

import { ReceiptUpload } from "@/components/receipt-upload"

With OCR, the form fills itself. Textract's expense model and Azure's prebuilt-receipt return the fields directly; other engines are read from the text:

import { ocrEndpoint } from "@uploadcn/core"

;<ReceiptUpload
  recognize={ocrEndpoint("/api/ocr")} // npx shadcn@latest add @uploadcn/ocr-route
  onSubmit={(expense) => createExpense(expense)}
/>

Or extract the fields yourself, with any API or model:

<ReceiptUpload
  extract={async (item) => {
    const response = await fetch(`/api/receipts/${item.result.key}/ocr`)
    return response.json() // { merchant, date, amount, category }
  }}
  onSubmit={(expense) => createExpense(expense)}
/>

On phones the picker opens the rear camera (capture="environment"). Fields the user already typed are never overwritten. receiptFromOcr(result) is exported if you want the same mapping elsewhere. See the OCR guide for providers.

Props

Prop

Type

On this page