> Documentation index: [/llms.txt](/llms.txt). Every page as one file: [/llms-full.txt](/llms-full.txt).

# Upload and organize files

Upload and organize files [#upload-and-organize-files]

Upload files to a workspace, organize them in folders, find them by name, and download them.

Reference [#reference]

`GET /files/` parameters [#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](#keyword-matching) rules do not apply here. |
| `limit`     | Page size, 1 to 1000. Default `100`.                                                                                                     |
| `offset`    | Pagination offset.                                                                                                                       |

Keyword matching [#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-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](#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 [#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 returns `400`.
* `cascade` defaults to `true`; with `cascade=false`, deleting a folder that is not empty returns `409`.

File endpoints [#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 `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](/docs/api/alo-files/sharepoint-sync) returns `409` with `details.reason` `managed_by_sync`.

Batch endpoints [#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 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 [#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-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 [#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 [#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 title="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](#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`.

   ```bash
   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 title="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`](#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 [#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](#get-files-parameters).

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

Create a folder [#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](#folder-endpoints).

   ```python title="Python"
   folder = httpx.post(
       f"{FILES_URL}/folders/",
       headers=HEADERS,
       json={"name": "Legal", "parent_id": "root"},
   ).json()
   ```

Build a file browser [#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 title="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}`.

   ```json
   {
     "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 [#download-a-file]

1. To get a download link, send `GET /files/{file_id}/download-url`.

   ```python title="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 [#troubleshooting]

Finalize returns `409 Conflict` [#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`, 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.

```json
{
  "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.
