# Add receipts from your app

> Upload receipts of any size with a private upload link, then follow them until they're filed.

Source: https://app.getoatmilk.com/docs/tutorials/add-receipts

Expense apps, scanners, shared drives and email tools all collect receipts. This tutorial sends them to Oatmilk the way the Oatmilk app does: prepare an upload, send the file to a private link, confirm it, and follow the job until the receipt is filed.

For files up to 2 MB, the one-call `uploads.inline` in the [quickstart](https://app.getoatmilk.com/docs/quickstart.md#4-add-a-receipt) is simpler. Use this flow for anything up to 50 MB, and when you want the file to travel separately from your API request.

## Try the flow

Choose a receipt from your computer to see every request with its real size and hash. The file stays in your browser: nothing is uploaded.

1. `uploads.prepare` with the file's name, type, size and SHA-256.
2. `PUT` the unchanged bytes to `uploadUrl` (skip when `alreadyUploaded` is true).
3. `uploads.confirm` with the `submissionId`.
4. `jobs.get` until `status` is `completed` or `failed`.

## 1. Prepare the upload

Tell Oatmilk what you're about to send: the file's name, type, size and SHA-256 hash. The hash lets Oatmilk prove the stored file is exactly the one you meant, and it makes retries safe.

```js title="prepare.js"
import { createHash, randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";

const bytes = await readFile("receipt.jpg");
const sha256 = createHash("sha256").update(bytes).digest("hex");
const idempotencyKey = randomUUID();
```

```bash
curl https://app.getoatmilk.com/api/v1/accounting/uploads.prepare \
  -H "Authorization: Bearer $OATMILK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "filename": "receipt.jpg",
  "mimeType": "image/jpeg",
  "sizeBytes": 182734,
  "sha256": "9f2b5c1d7e3a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c"
}'
```

```json
{
  "data": {
    "submissionId": "4d5e6f7a-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
    "uploadUrl": "https://storage.example.com/upload/sign/accounting/receipt.jpg?token=synthetic",
    "uploadToken": "synthetic-upload-token",
    "storagePath": "org_synthetic/4d5e6f7a-8b9c-4d0e-8f1a-2b3c4d5e6f7a/upload/9f2b5c1d7e3a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c/receipt.jpg",
    "expiresIn": 7200,
    "alreadyUploaded": false
  }
}
```

> [!NOTE]
> If `alreadyUploaded` is `true`, Oatmilk already has these exact bytes from an earlier attempt. Skip the next step, but still confirm.

## 2. Send the file

PUT the unchanged bytes to `uploadUrl` within two hours. Don't send your API key to this address: the link carries its own short-lived permission.

```js title="upload.js"
const put = await fetch(prepared.uploadUrl, { method: "PUT", headers: { "Content-Type": "image/jpeg" }, body: bytes });
if (!put.ok) throw new Error(`Upload failed with ${put.status}`);
```

## 3. Confirm it

Confirming checks the stored bytes against the size and hash you declared, keeps the original, and starts reading it.

```bash
curl https://app.getoatmilk.com/api/v1/accounting/uploads.confirm \
  -H "Authorization: Bearer $OATMILK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "submissionId": "4d5e6f7a-8b9c-4d0e-8f1a-2b3c4d5e6f7a"
}'
```

```json
{
  "data": {
    "submissionId": "4d5e6f7a-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
    "jobId": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
    "generation": 1,
    "status": "queued"
  }
}
```

## 4. Follow the job

Ask `jobs.get` until the job is `completed` or `failed`, waiting a little longer each time. Or subscribe to the `receipt.processed` [webhook](https://app.getoatmilk.com/docs/webhooks/events.md) and skip polling altogether.

```js title="wait.js"
for (let wait = 1; ; wait = Math.min(wait * 2, 30)) {
  const job = await callOatmilk("jobs.get", { jobId: confirmed.jobId });
  if (job.status === "completed") break;
  if (job.status === "failed") throw new Error(job.error_code ?? "Receipt processing failed");
  await new Promise(resolve => setTimeout(resolve, wait * 1000));
}
```

## Attach it to a purchase

If you know which card purchase the receipt is for, pass its entry ID as `receiptFor` when you prepare the upload. Oatmilk then compares the receipt with that purchase only, and refuses the upload if the purchase no longer needs one. `attention.mine` lists the caller's purchases that still need a receipt; see [Chase missing receipts](https://app.getoatmilk.com/docs/tutorials/missing-receipts.md).

## Recap

- Generate one idempotency key per file and reuse it for every retry of every step.
- Hash the exact bytes you upload. A different file needs a new key.
- A confirmed upload is safe in Oatmilk even while it's still being read.
