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. Keys have the form {business_id}/{uuid}-{filename} and are only usable by the business that uploaded them.
Every uploaded file is scanned for malware. A file that is still being scanned is refused by the endpoint that reads it with 400 ("File is still being scanned for malware. Please try again in a few seconds"), so retry after a moment; a file the scan flags is refused for good.
The
/api/v1/upload/*endpoints work with an API token but are not listed in the public API Reference. The endpoints that consume ans3_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
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 (trimmed; upload_fields also carries the policy and signature entries S3 needs):
{
"success": true,
"data": {
"s3_upload_url": "https://dayzero-uploads.s3.amazonaws.com/",
"upload_method": "POST",
"upload_fields": { "key": "0198a2c4-0000-7d3a-9a51-3f6f0c1d2e00/0198a2c4-aaaa-7d3a-9a51-3f6f0c1d2e01-receipt.pdf", "Content-Type": "application/pdf", "...": "..." },
"max_bytes": 262144000,
"file_format": "pdf",
"content_type": "application/pdf",
"s3_key": "0198a2c4-0000-7d3a-9a51-3f6f0c1d2e00/0198a2c4-aaaa-7d3a-9a51-3f6f0c1d2e01-receipt.pdf",
"filename": "receipt.pdf"
}
}| Field | Description |
|---|---|
s3_upload_url |
Presigned S3 POST URL, valid for one hour |
upload_method |
Always POST |
upload_fields |
Form fields that must accompany the file |
max_bytes |
Largest body S3 will accept (250 MB) |
s3_key |
The key to reference the file with |
content_type |
The type DayZero inferred from the filename; it is part of the signed policy |
file_format, filename, s3_bucket, business_id |
Detected extension, your filename, bucket and business |
Supported extensions: pdf, csv, xls, xlsx, doc, docx, md, markdown, txt, json, gz, image formats (png, jpg, jpeg, gif, webp, heic, heif, bmp, tif, tiff, avif) and video formats (mp4, m4v, mov, webm, avi, mkv, ogv, 3gp, mpeg, mpg, wmv). Anything else (including html and svg) is rejected with 400.
Step 2: Upload to S3
Send one multipart/form-data POST to s3_upload_url containing every entry of upload_fields, in the order returned, followed by the file as the form field file (the file must be the last part):
curl -X POST "{s3_upload_url}" \
-F "key=<upload_fields.key>" \
-F "Content-Type=application/pdf" \
-F "<every other upload_fields entry>=<its value>" \
-F "file=@receipt.pdf"S3 answers with a 2xx on success and rejects a body larger than max_bytes or a Content-Type that differs from the one in the policy. A PUT to this URL does not work.
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 |
| 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 |
| 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 |
| Feed a spreadsheet to a report | file_s3_key / s3_keys on POST /api/v1/reports — see 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):
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 s3_key, filename, file_size, business_id, status and message. The extension must be one of the supported types and the file's leading bytes must match it (400 VALIDATION_195 otherwise). Files over 250 MB are refused with 400 VALIDATION_054 (a request body over the server's size limit is refused with 413 before it is read; see Error Handling).
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:
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 and s3_key. The link is valid for 15 minutes and needs no DayZero headers. Report files (keys under reports/) are not served here; use GET /api/v1/reports/download (see Reports). Or stream the file through the API (avoids CORS issues with presigned URLs in browsers):
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.pdffilename 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
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, updated_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:
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.
Common errors
| Status | Cause | Fix |
|---|---|---|
400 |
Extension not supported (for example html, svg), or VALIDATION_195 (file contents do not match the extension) |
Convert the file to a supported type |
400 |
The file is still being scanned for malware, or was flagged by the scan | Retry after a few seconds; a flagged file is refused for good |
400 VALIDATION_054 |
File over 250 MB on /upload/direct |
Use the presigned upload |
4xx from S3 (not DayZero) |
upload_fields sent out of order or incomplete, the Content-Type changed, or the body is over max_bytes |
Send every field exactly as returned, with file last |
403 |
Download or stream of an s3_key that belongs to another business, or a key under reports/ on /upload/download |
Use the key from this business; reports go through GET /api/v1/reports/download |
404 |
/upload/stream for a key that does not exist |
Check the s3_key |