Skip to Content
Developer APIDrive API

Drive API

Base path: /api/v1/drive/
Plan feature required: drive
Auth: PAT bound to a user, or Keycloak user-delegated JWT

Drive operates on per-user data. Every request must resolve to an acting user. A pure service account token (client credentials without a bound user) is rejected with 403 Forbidden. Use a PAT with user_id or an OAuth user token.


Scopes

ScopeGrants
drive:readBrowse, download, list shares, list versions, delta sync
drive:writeUpload, create folders, rename, move, copy, share, trash, restore, permanently delete

Browse

# Root folder GET /api/v1/drive/browse # Specific folder GET /api/v1/drive/browse/{folder_id}

Query parameters: limit, offset

Response: FolderContents with breadcrumbs, pagination, and a list of FileItem objects.

curl https://api.eunifyer.com/api/v1/drive/browse \ -H "Authorization: Bearer $TOKEN"

GET /api/v1/drive/search?q=quarterly+report

Returns matched files and folders across the user’s Drive.


Storage usage

GET /api/v1/drive/storage

Returns { used_bytes, quota_bytes, file_count }.


File operations

Get file metadata

GET /api/v1/drive/{file_id}/detail

Download a file

GET /api/v1/drive/{file_id}/download

Returns { url: string } — a short-lived presigned S3 URL. Redirect to this URL to download the file directly (no auth needed for the S3 URL itself).

DOWNLOAD_URL=$(curl -s https://api.eunifyer.com/api/v1/drive/$FILE_ID/download \ -H "Authorization: Bearer $TOKEN" | jq -r .url) curl -L "$DOWNLOAD_URL" -o output.pdf

Upload a file (3-step)

Step 1 — Request an upload URL:

curl -X POST https://api.eunifyer.com/api/v1/drive/upload \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "report.pdf", "mime_type": "application/pdf", "size": 204800, "parent_id": "optional-folder-uuid" }'

Response: { "upload_url": "https://…", "file": { "id": "uuid", … } }

Step 2 — Upload the file bytes directly to S3 (no auth needed):

curl -X PUT "$UPLOAD_URL" \ -H "Content-Type: application/pdf" \ --data-binary @report.pdf

Step 3 — Confirm the upload:

curl -X POST https://api.eunifyer.com/api/v1/drive/$FILE_ID/confirm \ -H "Authorization: Bearer $TOKEN"

Returns the final FileItem. The file is now indexed and visible in Drive.

Resumable uploads (TUS)

For large files or unreliable connections, use the TUS protocol  at /api/v1/drive/tus/.

import * as tus from "tus-js-client"; const upload = new tus.Upload(file, { endpoint: "https://api.eunifyer.com/api/v1/drive/tus/", headers: { Authorization: `Bearer ${token}` }, metadata: { filename: file.name, filetype: file.type, parent_id: parentFolderId, }, chunkSize: 10 * 1024 * 1024, // 10 MB onProgress(bytesUploaded, bytesTotal) { console.log(`${Math.round((bytesUploaded / bytesTotal) * 100)}%`); }, onSuccess() { console.log("Upload complete:", upload.url); }, }); upload.start();

TUS requires drive:write on create/PATCH/DELETE and drive:read on HEAD.

Create a folder

curl -X POST https://api.eunifyer.com/api/v1/drive/folders \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Q1 Reports", "parent_id": "optional-folder-uuid" }'

Rename

PATCH /api/v1/drive/{file_id}/rename { "name": "Q1 Reports Final.pdf" }

Move

PATCH /api/v1/drive/{file_id}/move { "target_folder_id": "destination-folder-uuid" }

Copy

POST /api/v1/drive/{file_id}/copy { "target_folder_id": "optional-destination-folder-uuid" }

Trash and restore

POST /api/v1/drive/{file_id}/trash # Move to trash (recoverable) POST /api/v1/drive/{file_id}/restore # Restore from trash

Delete permanently

DELETE /api/v1/drive/{file_id} # Permanent deletion — 204 No Content

Sharing

curl -X POST https://api.eunifyer.com/api/v1/drive/$FILE_ID/shares \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "create_link": true, "permission": "view", "expires_at": "2026-12-31T23:59:59Z" }'

Response: ShareInfo with a share_link URL (public, no auth required to access).

List shares

GET /api/v1/drive/{file_id}/shares

Delete a share

DELETE /api/v1/drive/{file_id}/shares/{share_id}
GET /api/v1/drive/shared-link/{token}

Versions

List versions

GET /api/v1/drive/{file_id}/versions

Download a specific version

GET /api/v1/drive/{file_id}/versions/{version_id}/download

Returns a presigned URL (same as regular download).

Restore a version

POST /api/v1/drive/{file_id}/versions/{version_id}/restore

Delta sync

See Pagination & Delta Sync for a full guide.

Quick reference:

# Initial full snapshot GET /api/v1/drive/sync/delta?device_id=MY_DEVICE_ID # Incremental changes since cursor GET /api/v1/drive/sync/delta?device_id=MY_DEVICE_ID&cursor=opaque-cursor # Commit sync progress server-side POST /api/v1/drive/sync/commit { "device_id": "MY_DEVICE_ID", "cursor": "latest-cursor" }

Full upload + list example

import httpx, pathlib API = "https://api.eunifyer.com/api/v1" def upload_file(token: str, path: str, parent_id: str | None = None) -> dict: client = httpx.Client(base_url=API, headers={"Authorization": f"Bearer {token}"}) p = pathlib.Path(path) # Step 1: request upload URL r = client.post("/drive/upload", json={ "name": p.name, "mime_type": "application/octet-stream", "size": p.stat().st_size, **({"parent_id": parent_id} if parent_id else {}), }).raise_for_status().json() file_id = r["file"]["id"] # Step 2: PUT to S3 with open(path, "rb") as f: httpx.put(r["upload_url"], content=f.read()).raise_for_status() # Step 3: confirm return client.post(f"/drive/{file_id}/confirm").raise_for_status().json() def list_files(token: str, folder_id: str | None = None) -> list: client = httpx.Client(base_url=API, headers={"Authorization": f"Bearer {token}"}) path = f"/drive/browse/{folder_id}" if folder_id else "/drive/browse" return client.get(path).raise_for_status().json()["items"]

What’s not in v1

  • WebDAV Drive surface (planned, no confirmed timeline).
  • Machine-level Drive access without a bound user — bind a user_id to your PAT.
  • Real-time file change subscriptions via long-poll or SSE — use webhooks + delta sync instead.