Files and Documents
In Aerion, users, projects, clients, and expenses can have files associated with them — offers, purchase orders, contracts, NDAs, briefs, scanned IDs, certifications, receipts, and so on. Files never exist on their own; they always hang off one of these four record types.
The shape differs depending on which:
- Users, projects, and clients can have many Documents, each holding a single file plus its own metadata (name, category, date).
- Expenses carry one file, directly — no Document wrapper, the file attaches straight to the Expense.
This page covers the conceptual model and the gotchas you can't see in the API documentation. For full request/response schemas, follow the links into the Document, File, and Expense reference.
The Document object
For users, projects, and clients, the file isn't attached directly — there's a Document record in between. Each Document holds one file plus its descriptive metadata, and points at its parent via a (model, record) pair:
model | Parent | Typical use |
|---|---|---|
user | User | HR documents on an employee (IDs, certifications) |
project | Project | Offers, purchase orders, contracts, briefs |
client | Client | Client-level agreements, NDAs |
A single parent can have any number of Documents — each one is a separate record with its own ID, which is what you upload the file against. See POST /v1/documents for the full field list.
Expenses skip the Document wrapper. An Expense is a record type in its own right — a cost booked against a project — and its file (typically the receipt) attaches directly to the Expense ID. Same upload mechanics, just expense/{expenseId} instead of document/{documentId}, and only one file per Expense.
Profile pictures, client logos and account branding go through a separate /v1/images/... endpoint family that is not covered here.
The two-step upload flow
Every attachment is a two-step flow:
- Create the owning record — a Document via
POST /v1/documents, or an Expense viaPOST /v1/expenses. You get back the record'sid. - Upload the binary against that record via
POST /v1/files/upload/{model}/{record}. Themodelisdocumentorexpense;{record}is the ID from step 1. You get back an upload ID used for downloads and deletes.
If step 2 fails, delete the Document you created in step 1. The server does not clean up orphaned Document records, and they will show up in listings with no attached file.
Document categories
category is a numeric enum, scoped to the parent model. A user category will not validate against a project Document.
| ID | Key | Valid for model |
|---|---|---|
| 1 | employmentContract | user |
| 2 | targetAgreement | user |
| 3 | medicalCertificate | user |
| 4 | applicationDocuments | user |
| 5 | testimonials | user |
| 6 | miscellaneous | user |
| 7 | offer | project |
| 8 | clearance | project |
| 9 | nda | project |
| 10 | miscellaneous | project |
| 11 | contract | client |
| 12 | agreement | client |
| 13 | nda | client |
| 14 | miscellaneous | client |
visibleToUser
Only meaningful when model = "user": controls whether the employee can see their own document. Ignored on project and client Documents.
Uploading the file
Code
{model} is document or expense. The body is a multipart/form-data payload with a single file field carrying the binary. Full spec: Attach a file to a record.
A few things that aren't visible from the spec alone:
- Always send
?delete_others=1. Documents and Expenses accept only one file each. Without this flag, a second upload to the same record returns403 "too many uploads". With it, any previously attached file is replaced. - Filenames are sanitized. The server strips everything except letters, digits,
_,.and German umlauts (ä ö ü Ä Ö Ü ß). Dashes, spaces and punctuation are removed —invoice-2026 (final).pdfbecomesinvoice2026final.pdf. Read the returnedfilename; don't assume it matches what you sent. - Per-upload and per-account caps are enforced. Oversized files return
403; an account at its storage cap also returns403. Check with your account administrator for your specific limits. - Uploads may be rate limited. A
429response indicates you've hit the limit — wait a moment and retry. - Server-side clients must buffer the file before posting it. The backend reads the upload size from the
Content-Lengthof thefilepart. Streaming with chunked transfer encoding leaves it undefined and the upload fails validation with a misleading500 "Missing value for required attribute size". If your HTTP client streams by default (node-fetch+form-datawith a stream,undici,axioswith a stream body), buffer the file into memory first or pass an explicitknownLengthto your form library. Browsers andcurl -FsendContent-Lengthautomatically and aren't affected.
The response includes the upload ID, which is what you use for downloads and deletes — not the owning record's ID.
Listing what's attached
Code
Returns the upload(s) on a given record. Since Documents and Expenses carry at most one file each, this list is either empty or has a single entry. Full spec: List uploads attached to a record.
To list all Documents on a project (or client/user) use the Document endpoint with a where filter:
Code
The where value is JSON and must be URL-encoded. Full spec: List Document.
Downloading a file
Code
Returns a signed S3 URL as a JSON string. Full spec: Signed URL for downloading an upload.
- The URL is valid for 30 seconds. Download immediately — don't store or cache it.
- Issue a plain
GETagainst the URL. NoAuthorizationheader — the query-string signature grants access. - This endpoint may be rate limited; a
429indicates you've hit the limit — wait a moment and retry.
Deleting
Code
Removes the binary and the upload record. The owning Document or Expense is not deleted — remove it separately via DELETE /v1/documents/{id} or DELETE /v1/expenses/{id} if you want it gone too. Full spec: Delete an upload.
Error reference
| Status | Meaning | What to do |
|---|---|---|
400 | Malformed request (e.g. missing file field in the multipart body) | Check field names and content types |
401 | Missing or expired token | Refresh the OAuth token (see Authentication) |
403 "too many uploads" | Record already has a file and delete_others was not set | Add ?delete_others=1 |
403 (size/storage) | File exceeds per-upload or account storage limit | Reduce file size or contact your account admin |
404 | Record or upload not found, or no permission | Verify IDs and that the token has access |
429 | Rate limit exceeded | Wait a moment and retry |
End-to-end example
Attaching an offer PDF to project 1234:
Code
Attaching a receipt to an Expense is identical — create the Expense first via POST /v1/expenses, then POST /v1/files/upload/expense/{expenseId}?delete_others=1.