Groundfloor Docs

Files API

Presigned upload and download for workspace-scoped object storage.

Base path: /v1/workspaces/{workspace_id}/files

File contents never pass through the API. It keeps each file's details and gives you short-lived links; you upload and download directly from storage.

See also: Files

Upload lifecycle

sequenceDiagram
  participant Client
  participant CP as Control Plane
  participant S3 as Object Store

  Client->>CP: POST /files/upload-url
  CP-->>Client: presigned PUT URL + file_id
  Client->>S3: PUT bytes
  Client->>CP: POST /files/{id}/finalize
  CP->>S3: HEAD object
  CP-->>Client: active file metadata

List files

GET /v1/workspaces/{workspace_id}/files

Permission: read

Returns up to 200 active files, newest first, as { "files": [ … ] }.

Request upload URL

POST /v1/workspaces/{workspace_id}/files/upload-url

Creates a pending row and returns a presigned PUT URL (default TTL 3600s). Before it issues the URL, the API checks the declared size against the per-file limit and your organization's storage quota.

Permission: write

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"report.pdf","content_type":"application/pdf","size_bytes":1048576}' \
  "https://platform.groundfloor.cloud/v1/workspaces/$WORKSPACE_ID/files/upload-url" | jq .

Request body

FieldTypeRequiredDescription
namestringYesDisplay name, 1 to 255 characters. The storage key is chosen by the server
size_bytesintegerYesExact number of bytes you will upload, greater than 0. It is signed into the URL, so storage rejects an upload of a different size
content_typestringNoMIME type you will send, up to 255 characters. Stored as given

Response 200

{
  "file": { "id": "…", "name": "report.pdf", "status": "pending", "…": "…" },
  "upload_url": "https://…",
  "expires_in_s": 3600
}

Upload bytes with curl -X PUT -T report.pdf -H "Content-Type: application/pdf" "$UPLOAD_URL".

Limits

LimitValueResult when exceeded
Size of one file512 MiB413
Organization storage (files.bytes on its quota)Depends on plan507

Finalize upload

POST /v1/workspaces/{workspace_id}/files/{file_id}/finalize

HEADs the object in storage, captures true size/content-type, promotes row to active. Calling it again on an active file returns the file.

The real size is checked again here. If the uploaded object is over the 512 MiB limit or would push the organization over its storage quota, it is deleted and the call returns 413 or 507.

Permission: write

StatusMeaning
404File not found in this workspace
409The object isn't in storage yet; the upload may not have completed
410The file was deleted

Download URL

GET /v1/workspaces/{workspace_id}/files/{file_id}/download-url

Returns a short-lived presigned GET URL. Returns 409 if the file isn't active (for example, the upload hasn't been finalized).

Permission: read

Delete file

DELETE /v1/workspaces/{workspace_id}/files/{file_id}

Removes object from bucket and marks row deleted. Returns the file's metadata with status deleted; deleting an already-deleted file returns it again.

Permission: delete

Workspace-wide access for app roles

GET /v1/workspaces/{workspace_id}/files/acl
PUT /v1/workspaces/{workspace_id}/files/acl

Reads or replaces which of your app's sign-in roles can read or write files in the workspace. The body is { "grants": [ { "role": "admin", "action": "write" }, { "role": "viewer", "action": "read" } ] } and action is read or write. The built-in admin role is a role like any other; new workspaces start with admin → write. PUT replaces the whole list and saves it as sent, so if you leave admin out, admins lose access to Files. Permission: read for GET, ddl for PUT; site users get 403. The Access API manages the same grants alongside Data Vault and App access.

Object key namespacing

Server controls keys: workspaces/{workspace_id}/files/{file_id} — clients cannot target another workspace's prefix.