Alo Files

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}.

ParameterDescription
parent_idFolder ID or root. Omitted: root-level files. Only this folder is listed.
keywordCase-insensitive substring of the file name, within that folder only. The Keyword matching rules do not apply here.
limitPage size, 1 to 1000. Default 100.
offsetPagination offset.

Keyword matching

Rules for keyword on GET /contents (GET /files/ uses a plain substring match instead):

  • Accent- and case-insensitive: Peru matches Perù, RESP matches resp.
  • Split on spaces; a name matches only if it contains every word: Peru She matches Perù 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.

ModeReturnsfolder_path_segmentsbreadcrumb
No keywordSubfolders and files directly under parent_id (root if omitted)nullFolders from the root down to parent_id; [] at the root
With keywordMatching folders and files across the whole workspace, not just parent_id, per Keyword matchingPath 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

ActionEndpointBodyReturnsAuth
Create a folderPOST /folders/{name, parent_id}201, folderKey or bearer
List child foldersGET /folders/?parent_id=rootNoneList of foldersKey or bearer
Get a folderGET /folders/{folder_id}NoneFolder, with folder_path_segmentsKey or bearer
Rename a folderPATCH /folders/{folder_id}/rename{name}FolderKey or bearer
Move a folderPATCH /folders/{folder_id}/move{parent_id}FolderKey or bearer
Rename, move, or bothPATCH /folders/{folder_id}{rename: {name}, move: {parent_id}}FolderKey or bearer
Delete a folderDELETE /folders/{folder_id}?cascade=trueNone204Key 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 returns 400.
  • cascade defaults to true; with cascade=false, deleting a folder that is not empty returns 409.

File endpoints

ActionEndpointBodyReturnsAuth
Get a fileGET /files/{file_id}NoneFile, with folder_path_segments ([] for a root-level file)Key or bearer
Rename a filePATCH /files/{file_id}/rename{file_name}FileKey or bearer
Move a filePATCH /files/{file_id}/move{parent_id}FileKey or bearer
Duplicate a filePOST /files/{file_id}/duplicate{parent_id}, required; root or null for the root201, new fileKey or bearer
Delete a fileDELETE /files/{file_id}None204Key or bearer
  • Only indexed files can be duplicated; others return 409. The copy is indexed straight away.
  • A copy into the source's own folder is named <name> (n)<ext> with the smallest free n, such as report (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 409 with details.reason managed_by_sync.

Batch endpoints

EndpointBodyReturnsAuth
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 does root.
  • Deleting a file that is still processing also cancels its processing.
  • POST /batch/check always reports root as existing.

Copy from another workspace

EndpointReturnsAuth
POST /files/{file_id}/copy-to-space201 with the new file, or 200 with the earlier copy if this file was already copied hereBearer only
POST /folders/{folder_id}/copy-to-space201 with the target folderBearer only
  • The body is {source_space_slug, parent_id}. X-Space-Slug names the target workspace and parent_id a folder in it (root or null for 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 is 403.
  • Only indexed files are copied; copying a file in another status returns 409. A folder copy skips files that are not yet indexed and reuses an earlier copy of the same folder.
  • A source workspace equal to the target returns 409; use duplicate instead.

Initiate errors

StatusWhen
400 unsupported_mimeThe file type is not supported; details.allowed lists the accepted MIME types
402The uploader's plan does not allow the upload; the body is Central Hub's 402 error, passed through
404 not_foundThe parent_id folder does not exist in this workspace

Upload a file

  1. To start an upload, send POST /files/initiate with the file name, MIME type, size, and target folder. parent_id is a folder ID, root, or null.

    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_id and upload_url. For errors, see Initiate errors.

  2. To send the file's bytes, PUT them to upload_url. The request goes to storage, so it only needs the correct Content-Type.

    curl -X PUT -H "Content-Type: application/pdf" --data-binary @contracts.pdf "<upload_url>"
  3. 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 Accepted while processing continues. If it is 409 Conflict, read details.reason and resolve it with Finalize returns 409 Conflict.

  4. To know when the file is ready, poll GET /files/{file_id} until status is indexed or error.

List files

  1. To list the files in a folder, send GET /files/ with the folder's parent_id (or root) and a limit. All parameters are in GET /files/ parameters.

    Python
    files = httpx.get(
        f"{FILES_URL}/files/",
        headers=HEADERS,
        params={"parent_id": "root", "limit": 100},
    ).json()

Create a folder

  1. To create a folder, send POST /folders/ with its name and parent_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

  1. To get a folder's subfolders and files in one response, send GET /contents with parent_id. To match folder and file names across the whole workspace instead, add keyword. Each item's kind says 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"])
  2. To show where a keyword match lives, read its folder_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 are null for root-level files in GET /contents, and [] for them in GET /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" }
        ]
      }
    }
  3. To open a matched file's folder, use the file's parent_id (equal to the last segment's folder_id).

  4. To show a folder page with breadcrumbs, send GET /contents with that parent_id and no keyword, and read the top-level breadcrumb.

Download a file

  1. 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"])
  2. To download the original uploaded file, fetch url from the response. The link is temporary.

Troubleshooting

Finalize returns 409 Conflict

Read details.reason to tell the two causes apart:

details.reasonCause
duplicate_in_folderThe 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, or indexing also 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:

ChoiceHow
Keep bothFinalize details.uploaded_file_id again with {"skip_duplication": true}
ReplaceDelete 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.

On this page