Upload and organize files
Upload files to a workspace, organize them in folders, find them by name, and download them.
Reference
GET /files/ parameters
GET /files/ lists the files in one folder, newest first, as {items, total, limit, offset}.
| Parameter | Description |
|---|---|
parent_id | Folder ID or root. Omitted: root-level files. Only this folder is listed. |
keyword | Case-insensitive substring of the file name, within that folder only. The Keyword matching rules do not apply here. |
limit | Page size, 1 to 1000. Default 100. |
offset | Pagination offset. |
Keyword matching
Rules for keyword on GET /contents (GET /files/ uses a plain substring match instead):
- Accent- and case-insensitive:
PerumatchesPerù,RESPmatchesresp. - Split on spaces; a name matches only if it contains every word:
Peru ShematchesPerù Sheet.xlsx. - Word order and extra spaces are ignored.
- Folder names match as well as file names.
GET /contents modes
GET /contents returns folders and files in one response: {parent_id, breadcrumb, items, total, limit, offset}, folders first. limit is 1 to 1000, default 200.
| Mode | Returns | folder_path_segments | breadcrumb |
|---|---|---|---|
No keyword | Subfolders and files directly under parent_id (root if omitted) | null | Folders from the root down to parent_id; [] at the root |
With keyword | Matching folders and files across the whole workspace, not just parent_id, per Keyword matching | Path from the root on each match; null for root-level files | [] |
GET /folders/{folder_id}/contents (folder_id can be root) has a different shape: {parent_id, breadcrumb, folders, files}, with no paging and at most 1000 files.
Folder endpoints
| Action | Endpoint | Body | Returns | Auth |
|---|---|---|---|---|
| Create a folder | POST /folders/ | {name, parent_id} | 201, folder | Key or bearer |
| List child folders | GET /folders/?parent_id=root | None | List of folders | Key or bearer |
| Get a folder | GET /folders/{folder_id} | None | Folder, with folder_path_segments | Key or bearer |
| Rename a folder | PATCH /folders/{folder_id}/rename | {name} | Folder | Key or bearer |
| Move a folder | PATCH /folders/{folder_id}/move | {parent_id} | Folder | Key or bearer |
| Rename, move, or both | PATCH /folders/{folder_id} | {rename: {name}, move: {parent_id}} | Folder | Key or bearer |
| Delete a folder | DELETE /folders/{folder_id}?cascade=true | None | 204 | Key or bearer |
- Creating or renaming a folder with a name a sibling folder already uses returns
409. Moving a folder into itself or its subfolders returns400. cascadedefaults totrue; withcascade=false, deleting a folder that is not empty returns409.
File endpoints
| Action | Endpoint | Body | Returns | Auth |
|---|---|---|---|---|
| Get a file | GET /files/{file_id} | None | File, with folder_path_segments ([] for a root-level file) | Key or bearer |
| Rename a file | PATCH /files/{file_id}/rename | {file_name} | File | Key or bearer |
| Move a file | PATCH /files/{file_id}/move | {parent_id} | File | Key or bearer |
| Duplicate a file | POST /files/{file_id}/duplicate | {parent_id}, required; root or null for the root | 201, new file | Key or bearer |
| Delete a file | DELETE /files/{file_id} | None | 204 | Key or bearer |
- Only
indexedfiles can be duplicated; others return409. The copy isindexedstraight away. - A copy into the source's own folder is named
<name> (n)<ext>with the smallest freen, such asreport (1).pdf. A copy into another folder keeps the source name, even if that folder already has a file with that name. - Renaming or moving a file or folder imported by SharePoint sync returns
409withdetails.reasonmanaged_by_sync.
Batch endpoints
| Endpoint | Body | Returns | Auth |
|---|---|---|---|
POST /batch/delete | {file_ids, folder_ids}, at least one ID | {files: {deleted, not_found}, folders: {deleted, not_found}} | Key or bearer |
POST /batch/check | {file_ids, folder_ids} | {existing_file_ids, existing_folder_ids} | Key or bearer |
- Folders are deleted with everything inside them. IDs that are invalid, missing, or in another workspace come back in
not_found, as doesroot. - Deleting a file that is still processing also cancels its processing.
POST /batch/checkalways reportsrootas existing.
Copy from another workspace
| Endpoint | Returns | Auth |
|---|---|---|
POST /files/{file_id}/copy-to-space | 201 with the new file, or 200 with the earlier copy if this file was already copied here | Bearer only |
POST /folders/{folder_id}/copy-to-space | 201 with the target folder | Bearer only |
- The body is
{source_space_slug, parent_id}.X-Space-Slugnames the target workspace andparent_ida folder in it (rootornullfor its root). - Needs a signed-in user's bearer token; an API key gets
403. The user must be a member of the source workspace, or the response is403. - Only
indexedfiles are copied; copying a file in another status returns409. A folder copy skips files that are not yetindexedand reuses an earlier copy of the same folder. - A source workspace equal to the target returns
409; use duplicate instead.
Initiate errors
| Status | When |
|---|---|
400 unsupported_mime | The file type is not supported; details.allowed lists the accepted MIME types |
402 | The uploader's plan does not allow the upload; the body is Central Hub's 402 error, passed through |
404 not_found | The parent_id folder does not exist in this workspace |
Upload a file
-
To start an upload, send
POST /files/initiatewith the file name, MIME type, size, and target folder.parent_idis a folder ID,root, ornull.Python import httpx FILES_URL = "https://alo-files-service-103105631559.europe-west8.run.app" HEADERS = { "X-API-Key": "sk-your-api-key", "X-Space-Slug": "acme", } upload = httpx.post( f"{FILES_URL}/files/initiate", headers=HEADERS, json={ "file_name": "contracts.pdf", "mime_type": "application/pdf", "size_bytes": 1842030, "parent_id": "root", }, ).json()The response contains
file_idandupload_url. For errors, see Initiate errors. -
To send the file's bytes,
PUTthem toupload_url. The request goes to storage, so it only needs the correctContent-Type.curl -X PUT -H "Content-Type: application/pdf" --data-binary @contracts.pdf "<upload_url>" -
To start processing, finalize the file with
POST /files/{file_id}/finalize.Python httpx.post(f"{FILES_URL}/files/{upload['file_id']}/finalize", headers=HEADERS)The response is
202 Acceptedwhile processing continues. If it is409 Conflict, readdetails.reasonand resolve it with Finalize returns409 Conflict. -
To know when the file is ready, poll
GET /files/{file_id}untilstatusisindexedorerror.
List files
-
To list the files in a folder, send
GET /files/with the folder'sparent_id(orroot) and alimit. All parameters are inGET /files/parameters.Python files = httpx.get( f"{FILES_URL}/files/", headers=HEADERS, params={"parent_id": "root", "limit": 100}, ).json()
Create a folder
-
To create a folder, send
POST /folders/with itsnameandparent_id. To list, get, rename, move, or delete folders, use the folder endpoints.Python folder = httpx.post( f"{FILES_URL}/folders/", headers=HEADERS, json={"name": "Legal", "parent_id": "root"}, ).json()
Build a file browser
-
To get a folder's subfolders and files in one response, send
GET /contentswithparent_id. To match folder and file names across the whole workspace instead, addkeyword. Each item'skindsays whether it is a folder or a file.Python contents = httpx.get( f"{FILES_URL}/contents", headers=HEADERS, params={"parent_id": "root", "keyword": "invoice"}, ).json() for item in contents["items"]: if item["kind"] == "folder": print("Folder:", item["folder"]["name"]) else: print("File:", item["file"]["file_name"]) -
To show where a
keywordmatch lives, read itsfolder_path_segments. The segments run from the root down to the file's containing folder (for a matched folder, down to the folder itself). They arenullfor root-level files inGET /contents, and[]for them inGET /files/{file_id}.{ "kind": "file", "file": { "file_id": "…", "file_name": "Perù Sheet.xlsx", "parent_id": "9b1f6c2e-4a7d-4e1b-8c3f-2d5a6e7f8091", "folder_path_segments": [ { "folder_id": "3c1d9a52-7b4e-4f0a-9d21-5e8f6a0b7c13", "name": "Clients" }, { "folder_id": "9b1f6c2e-4a7d-4e1b-8c3f-2d5a6e7f8091", "name": "Peru" } ] } } -
To open a matched file's folder, use the file's
parent_id(equal to the last segment'sfolder_id). -
To show a folder page with breadcrumbs, send
GET /contentswith thatparent_idand nokeyword, and read the top-levelbreadcrumb.
Download a file
-
To get a download link, send
GET /files/{file_id}/download-url.Python download = httpx.get(f"{FILES_URL}/files/{file_id}/download-url", headers=HEADERS).json() print(download["url"]) -
To download the original uploaded file, fetch
urlfrom the response. The link is temporary.
Troubleshooting
Finalize returns 409 Conflict
Read details.reason to tell the two causes apart:
details.reason | Cause |
|---|---|
duplicate_in_folder | The target folder already holds a file with the same (file_name, mime_type) that is not in error |
None (details is null) | The file is no longer uploaded, for example because it was already finalized |
For a duplicate:
- The check is per folder: the same name in another folder or workspace is a new file.
- Files still
uploaded,parsing, orindexingalso block, so concurrent uploads of the same file collapse into one. - The uploaded file stays
uploaded, so you can still finalize it.
{
"code": "conflict",
"message": "A file named 'contracts.pdf' already exists in this folder",
"details": {
"reason": "duplicate_in_folder",
"duplicate": {
"file_id": "…",
"file_name": "contracts.pdf",
"parent_id": "…",
"status": "indexed"
},
"uploaded_file_id": "…"
}
}On a duplicate, let the user choose:
| Choice | How |
|---|---|
| Keep both | Finalize details.uploaded_file_id again with {"skip_duplication": true} |
| Replace | Delete details.duplicate.file_id with DELETE /files/{file_id}, then finalize details.uploaded_file_id again |
skip_duplication skips only the name check; the file must still be uploaded to be finalized. Use it also to keep a same-name file on purpose, such as an automated re-ingest.