Skip to main content
The Company Files API manages files and their folder organization. File bytes never travel through the API itself — uploads and downloads both go straight to cloud storage via short-lived signed URLs, while the API handles metadata, placement, and permissions as plain JSON.
This API was rebuilt on 2026-06-10 with breaking changes: the multipart upload and the binary /content download were replaced by the signed-URL workflow below, and every response shape changed. See the changelog for the full diff.

Key Concepts

  • Owner — every file and folder belongs to an owner: a configured Field Model V1 entity (entityType + entityId, e.g. entityType: "exposure") or the whole company (entityType: "company", no entityId). entityType is the lowercase kebab-case wire slug — PascalCase like Exposure is rejected with a 400.
  • File — a display identity (displayName, renameable) over an immutable uploaded version (original fileName, contentType, byteSize). A file’s status is pending from upload intent until finalize, then ready — unless malware scanning holds it quarantined (scan verdict outstanding) or marks it infected (downloads permanently refused).
  • Version — every upload is an immutable version; a file keeps its full version history. Re-uploading (a new-version intent → PUT → the same finalize) adds a version and repoints the file at it, so the file keeps its id, placements, categories, and history — the download URL always serves the current version. List a file’s versions to see its history, with the current one flagged.
  • Placement — one appearance of a file under an owner. A file can be placed on several entities at once: sharing adds a placement, never a copy — every placement sees the same current version and history. Folder location and category are per placement; displayName is per file. Removing a placement detaches the file from that owner; the file itself is deleted only when its last placement is removed.
  • Category — a label on a placement, stored as free text. Each entity type has its own configured category list, which company admins manage in Company Settings → File Categories — Policy categories and Event categories are independent lists, and a company that has configured none simply has an empty list. GET /v1/files/categories?entityType=<slug> returns that type’s configured categories in their configured order plus the values in use on its placements that the list does not cover, each flagged so you can tell them apart; without the param it returns the distinct categories across the company’s live placements. Writes are never validated against the configured list — setting a category accepts any label up to 255 characters — so draw from the returned vocabulary to keep it consistent. See File categories below.
  • Folder — a named node in the owner’s folder tree (parentFolderId null = top level). A folder only ever holds files and subfolders of its own owner.
  • Signed URLs — uploads PUT to a signed uploadUrl pinned to the declared content type and byte size; downloads GET a signed url minted per request. Both expire after 15 minutes — request fresh ones, never cache them.
  • Soft delete — file and folder deletes are soft; folder deletes cascade recursively through the subtree and its files.

API Endpoints

Files

Folders

Permissions

Upload Workflow

Uploading is a three-step handshake; only steps 1 and 3 touch this API. 1. Declare the upload (intent)
2. PUT the bytes to the signed URL (direct to storage). Two headers are signed into the URL and must be sent exactly: the Content-Type you declared, and x-goog-content-length-range: <byteSize>,<byteSize>:
3. Finalize (flips the file from pending to ready):
If you abandon an upload after step 1, the pending file stays invisible and is reclaimed automatically — finalize is what publishes it.

Download Workflow

Mint a signed URL, then fetch the bytes from storage:
Batch — an entity’s whole document set in one call. List the entity’s files (each row carries its id), then mint every URL in a single POST instead of one round trip per file. Send up to 100 fileIds; add "disposition": "inline" to preview rather than download. The response pairs each url with its fileId, using the same per-file fields as the single-file endpoint:
The batch is all-or-nothing: if any id is unknown, belongs to another company, or points at a file that is not ready, the whole request is rejected (404 or 409) and no URLs are returned — a mixed batch can never partially leak. An empty fileIds array or more than 100 ids returns 400.

Re-upload Workflow (new version)

To replace a file’s contents without losing its id, placements, categories, or history, upload a new version. It is the same two-phase handshake as a first upload, but the intent targets the existing file — and it reuses the same finalize endpoint. Re-upload is gated by company.file:update (it changes an existing file), not company.file:create. 1. Declare the new version (re-upload intent)
2. PUT the bytes to the signed URL exactly as in the upload workflow (the two signed headers Content-Type and x-goog-content-length-range are required). 3. Finalize with the SAME endpoint — there is no separate re-upload finalize. Pass the new versionId; finalize flips the version ready and repoints the file at it, so the new bytes silently become current:
Inspect the history. List the file’s versions — newest first, the current one flagged:
A subsequent download URL serves the new current version. The file’s placements and categories are untouched. Roll back to a previous version. If a re-upload was a mistake, restore an earlier ready version to make it current again — pure metadata, so no bytes move, nothing is deleted, and the version you were on stays in history. Take a versionId from the version listing and restore it (gated by the same company.file:update):
The download URL now serves the restored version’s bytes. Only a ready version can be restored; restoring a pending version or the one that is already current returns 409, and an unknown version returns 404.

Files on entities

Files rarely live at the company level — most belong on an entity. Name the owner with entityType + entityId, where entityType is the entity’s lowercase kebab-case wire slug (event, exposure, quote, submission, person, organization, or policy). PascalCase like Quote is rejected with a 400 — always send the slug. Upload a file onto a quote. The handshake is the same three steps as the Upload Workflow above; only the intent changes — it names the owning entity instead of "company":
PUT the bytes to the signed URL and finalize exactly as in the upload workflow. List the quote’s files. The owner is selected by the entityType + entityId query pair (only ready files are returned):
Share the file onto the policy. Add a placement rather than uploading the document twice — the bytes are stored once and both the quote and the policy see the same version:
See Sharing a File Across Entities below for listing every placement, categorizing per placement, and detaching a file from one owner.

File categories

A category is a label on a placement (so a shared file can be categorized differently under each owner), stored as free text. Company admins configure a category list per entity type in Company Settings → File Categories; those lists give the vocabulary its membership and its order. Read one entity type’s vocabulary. Pass entityType — the same lowercase kebab-case owner slug the rest of this API uses (company, quote, policy, exposure, event, submission, person, organization). PascalCase like Quote is rejected with a 400:
entityTypeCategories lists the configured categories first, in the order the admin arranged them, followed by every label actually in use on that entity type’s live placements that no configured category covers — "configured": false. Those unconfigured values are real: an import, an API write, or a category an admin has since removed. They stay visible and filterable rather than disappearing. categories repeats the same names in the same order without the flags, for consumers that only need the list. Each list is scoped to one entity type, so a Policy category never appears under Event. Read the whole company’s in-use vocabulary. Omit entityType:
This is the distinct set of non-null categories across every owner’s live placements, each value once, sorted case-insensitively — a category that survives only on a soft-deleted placement (or a soft-deleted file) is excluded. It does not consult the configured lists, so a configured category no file uses yet does not appear here. Writes accept any label. Setting a category via Update File or Update File Placement is never validated against the configured list — any non-blank string up to 255 characters is accepted (blank clears the category). Read the vocabulary and reuse its values to keep it consistent; nothing rejects a new label.

Organizing with Folders

Only the requesting owner’s placement of each file moves — a file shared onto other entities keeps its placements there. The batch is capped at 100 file ids.

Sharing a File Across Entities

The same document often belongs on more than one entity — a loss run on both an Event and its Submission. Add a placement instead of uploading twice; the bytes are stored once and every placement stays in sync:
Sharing is same-company only, and a duplicate share to an owner the file is already placed on returns 409 Conflict. Removing the file’s last placement deletes the file itself and reclaims its stored content ("fileDeleted": true). Folder location and category are per placement, so once a file is shared the owner-less PATCH /v1/files/{fileId} can no longer address them — it returns 409 Conflict pointing at the per-placement update; displayName stays on the file (one name everywhere) and remains renameable there.