# File Uploads

> Upload files via presigned URLs or direct upload, and download attachments.

- **Category:** Guides
- HTML version: https://www.ondayzero.com/docs/guides/uploads
- All guides: https://www.ondayzero.com/docs/guides/index.md
- Endpoint catalog: https://www.ondayzero.com/docs/reference/index.md

---

# File Uploads

DayZero uses S3-backed file storage for bill attachments, bank statements, CSV/Excel imports, and other documents. Uploading is a two-step pattern: put the file in storage to get an `s3_key`, then pass that key to the endpoint that consumes it.

> The `/api/v1/upload/*` endpoints work with an API token but are not listed in the public API Reference. The endpoints that *consume* an `s3_key` (bills, bulk uploads, reconciliations, reports…) are all public and documented there.

## Presigned Upload (Recommended)

Get a presigned upload URL, then upload the file directly to S3 from the client:

### Step 1: Request a presigned URL

```bash
curl -X POST "https://api.ondayzero.com/api/v1/upload" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"filename": "receipt.pdf"}'
```

Returns `s3_upload_url` (a presigned S3 `PUT` URL, valid for one hour), the `s3_key` to reference the file with, and the `content_type` DayZero inferred from the filename. Supported extensions: `pdf`, `csv`, `xls`, `xlsx`, `doc`, `docx`, `md`, and common image formats (`png`, `jpg`, `jpeg`, `gif`, `webp`, `heic`, `heif`, `bmp`, `tiff`, `avif`); anything else is rejected with `400`.

### Step 2: Upload to S3

```bash
curl -X PUT "{s3_upload_url}" \
  -H "Content-Type: application/pdf" \
  --data-binary @receipt.pdf
```

Send the same `Content-Type` the presign response returned — S3 rejects the upload if it differs.

### Step 3: Use the `s3_key`

Pass the `s3_key` to whichever endpoint consumes it. Common ones:

| Use | Endpoint and field |
|-----|--------------------|
| Attach a document to a bill | `s3_key` on `POST /api/v1/bills`, `POST /api/v1/bills/received`, `PUT /api/v1/bills/{bill_id}` — see [Bills](/docs/guides/bills) |
| Create bills / invoices from PDFs with AI | `single_bill_s3_keys` / `multi_bill_s3_key` on `POST /api/v1/bills/ai-create`; `single_invoice_s3_keys` / `multi_invoice_s3_key` on `POST /api/v1/invoices/ai-create` |
| Import bank transactions from CSV/Excel | `s3_key` on `POST /api/v1/transactions/bulk-upload/headers` → `/preview` → `/bulk-upload` — see [Transactions](/docs/guides/transactions) |
| Bulk-import invoices, bills or journal entries | `s3_key` on `POST /api/v1/invoices/bulk`, `POST /api/v1/bills/bulk`, `POST /api/v1/journal-entries/bulk-upload/preview` and `/post` |
| Upload a bank statement for reconciliation | `statement_s3_key` on `POST /api/v1/reconciliations` — see [Reconciliation](/docs/guides/reconciliation) |
| Feed a spreadsheet to a report | `file_s3_key` / `s3_keys` on `POST /api/v1/reports` — see [Reports](/docs/guides/reports) |
| Vendor contracts and note attachments | `s3_key` on `POST /api/v1/vendor-contracts`, `POST /api/v1/notes/{note_id}/attachments` |

## Direct Upload

Upload a file through the API in one call (simpler, but the bytes transit DayZero's servers, so prefer presigned URLs for large files):

```bash
curl -X POST "https://api.ondayzero.com/api/v1/upload/direct" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -F "file=@receipt.pdf"
```

Returns the `s3_key`, `filename` and `file_size`. Oversized files are rejected with `413`.

Some workflows have their own multipart shortcut that uploads and processes in one request, e.g. `POST /api/v1/bills/upload` (bill document) and `POST /api/v1/reconciliations/upload` (bank statement).

## Download a File

Get a presigned download URL:

```bash
curl "https://api.ondayzero.com/api/v1/upload/download?s3_key=path/to/file.pdf" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
```

Returns `download_url`, a time-limited S3 link that needs no DayZero headers. Or stream the file through the API (avoids CORS issues with presigned URLs in browsers):

```bash
curl "https://api.ondayzero.com/api/v1/upload/stream?s3_key=path/to/file.pdf&filename=receipt.pdf" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -o downloaded-file.pdf
```

`filename` is optional and sets the download's `Content-Disposition` name. Only keys belonging to the business in `x-business-id` can be downloaded.

## List Uploaded Files

```bash
curl "https://api.ondayzero.com/api/v1/upload/files" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
```

Returns `items` of `{id, name, s3_key, type, source, created_at}`. Filter with `?source=user_upload` or `?source=ai_generated` to separate files you uploaded from ones DayZero generated.

## CSV Templates

Download templates for bulk imports. `GET /api/v1/upload/templates` lists the names — currently `rec-txns` (bank transactions), `bulk-invoice-upload`, `bulk-bill-upload` and `bulk-journal-entries`:

```bash
curl "https://api.ondayzero.com/api/v1/upload/templates/bulk-invoice-upload" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
```

The response carries the template as `csv_content_stringified` (plain CSV text), `csv_content_object` (rows as JSON) and `csv_content_base64`; the journal-entries template is also returned as `xls_content_base64`, pre-filled with the business's own ledger names.
