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 withuser_idor an OAuth user token.
Scopes
| Scope | Grants |
|---|---|
drive:read | Browse, download, list shares, list versions, delta sync |
drive:write | Upload, 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"Search
GET /api/v1/drive/search?q=quarterly+reportReturns matched files and folders across the user’s Drive.
Storage usage
GET /api/v1/drive/storageReturns { used_bytes, quota_bytes, file_count }.
File operations
Get file metadata
GET /api/v1/drive/{file_id}/detailDownload a file
GET /api/v1/drive/{file_id}/downloadReturns { 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.pdfUpload 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.pdfStep 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 trashDelete permanently
DELETE /api/v1/drive/{file_id} # Permanent deletion — 204 No ContentSharing
Create a share link
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}/sharesDelete a share
DELETE /api/v1/drive/{file_id}/shares/{share_id}Access a shared link (no auth)
GET /api/v1/drive/shared-link/{token}Versions
List versions
GET /api/v1/drive/{file_id}/versionsDownload a specific version
GET /api/v1/drive/{file_id}/versions/{version_id}/downloadReturns a presigned URL (same as regular download).
Restore a version
POST /api/v1/drive/{file_id}/versions/{version_id}/restoreDelta 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_idto your PAT. - Real-time file change subscriptions via long-poll or SSE — use webhooks + delta sync instead.