cirrux

Drive API

Manage a user’s Drive over JSON: list, upload, download, organize, trash and delete files and folders, and keep an external client in sync with a delta changes feed. All endpoints are under https://api.cirrux.co/public_api/v1/drive.

Every Drive endpoint needs a user-scoped OAuth access token (see Authentication) — files belong to a person, so workspace API keys are rejected. Each endpoint states the scope it requires (drive.read, drive.create, drive.update or drive.delete). File contents are end-to-end encrypted; the simple upload/download routes handle encryption server-side, while the chunked routes let a client encrypt and stream ciphertext directly.

The file object

FieldTypeRequiredDescription
objectstringnoAlways drive_file.
uuidstringnoUnique identifier of the file.
folder_uuidstring | nullnoParent folder, or null at the Drive root.
namestringnoFile name, unique among live siblings in its folder.
content_typestringnoMIME type.
file_size_bytesnumbernoPlaintext size in bytes.
upload_statusstringnouploading while bytes are still being written, completed once the file is downloadable.
trashedbooleannoWhether the file is in the trash (recoverable).
has_previewbooleannoWhether a server-rendered thumbnail is available (see Thumbnails). Images only for now; other types are always false.
versionnumbernoMetadata version — a per-item counter incremented on every change (rename, move, trash…).
ctagnumbernoContent version. Bumps only when the file’s bytes change. Files are immutable in place today (an edit creates a new file), so ctag is currently always 0.
created_atstringnoISO-8601 timestamp.
updated_atstringnoISO-8601 timestamp.
my_rolestringnoYour effective role on this item: viewer, commenter, editor, manager or owner. An item inside a shared folder inherits the folder’s role.
sharedbooleannoWhether a share was made directly on this item (a share root). Read with owner.self to tell “shared by you” (owner.self is true) from “shared with you” (false).
ownerobjectnoWho owns the item. Fields below.
owner.selfbooleannoWhether you are the owner.
owner.first_namestring | nullnoOwner’s first name, when set.
owner.last_namestring | nullnoOwner’s last name, when set.
owner.display_namestring | nullnoOwner’s full name, when set.

Use version as the item’s metadata version and ctag as its content version (e.g. File Provider’s metadataVersion / contentVersion). Keying the content version off version would re-download the whole file on every rename; ctag avoids that.

{
  "object": "drive_file",
  "uuid": "a1d9e7c8-2222-4f6a-9b1e-000000000002",
  "folder_uuid": "b3f1c2a4-1111-4a2b-8c3d-000000000001",
  "name": "april-2026.pdf",
  "content_type": "application/pdf",
  "file_size_bytes": 502998,
  "upload_status": "completed",
  "trashed": false,
  "has_preview": false,
  "version": 2,
  "ctag": 0,
  "created_at": "2026-06-19T09:13:40.000Z",
  "updated_at": "2026-06-19T11:02:17.000Z",
  "my_role": "owner",
  "shared": true,
  "owner": {
    "self": true,
    "first_name": "Jane",
    "last_name": "Doe",
    "display_name": "Jane Doe"
  }
}

The folder object

FieldTypeRequiredDescription
objectstringnoAlways drive_folder.
uuidstringnoUnique identifier of the folder.
parent_uuidstring | nullnoParent folder, or null at the Drive root.
namestringnoFolder name, unique among live siblings.
colorstring | nullnoFolder color as a #RRGGBB hex string, or null for none.
trashedbooleannoWhether the folder is in the trash.
versionnumbernoPer-item counter, incremented on every change.
created_atstringnoISO-8601 timestamp.
updated_atstringnoISO-8601 timestamp.
my_rolestringnoYour effective role on this item: viewer, commenter, editor, manager or owner. An item inside a shared folder inherits the folder’s role.
sharedbooleannoWhether a share was made directly on this item (a share root). Read with owner.self to tell “shared by you” (owner.self is true) from “shared with you” (false).
ownerobjectnoWho owns the item. Fields below.
owner.selfbooleannoWhether you are the owner.
owner.first_namestring | nullnoOwner’s first name, when set.
owner.last_namestring | nullnoOwner’s last name, when set.
owner.display_namestring | nullnoOwner’s full name, when set.

Files

List files & folders

GET/v1/drive/files

Lists the immediate children of a folder. Pass folder_uuidas a query parameter to list a folder’s contents; omit it to list the Drive root. Trashed items are excluded. Scope drive.read.

FieldTypeRequiredDescription
folder_uuidstringnoQuery parameter. The folder to list; omit for the root.
curl "https://api.cirrux.co/public_api/v1/drive/files?folder_uuid=b3f1c2a4-…" \
  -H "Authorization: Bearer $CIRRUX_TOKEN"
{
  "object": "list",
  "url": "/v1/drive/files",
  "folder_uuid": "b3f1c2a4-1111-4a2b-8c3d-000000000001",
  "folders": [ { "object": "drive_folder", "uuid": "…", "name": "Receipts", … } ],
  "files":   [ { "object": "drive_file",   "uuid": "…", "name": "april-2026.pdf", … } ]
}

List trash

GET/v1/drive/trash

Lists everything currently in the trash: every trashed (but not yet permanently deleted) folder and file you can see, flat across the whole workspace. Trash is a special folder, so the response mirrors the /v1/drive/files envelope. Restore an item with POST /v1/drive/files/{uuid} or /v1/drive/folders/{uuid} and trashed: false. Scope drive.read.

curl https://api.cirrux.co/public_api/v1/drive/trash \
  -H "Authorization: Bearer $CIRRUX_TOKEN"
{
  "object": "list",
  "url": "/v1/drive/trash",
  "folders": [ { "object": "drive_folder", "uuid": "…", "name": "Old receipts", "trashed": true, … } ],
  "files":   [ { "object": "drive_file",   "uuid": "…", "name": "draft.pdf",     "trashed": true, … } ]
}

Retrieve a file

GET/v1/drive/files/{uuid}

Returns a single drive_file. Scope drive.read.

Upload a file

POST/v1/drive/files

Uploads a file whose bytes you send inline as base64. Cirrux encrypts and stores it. This route buffers the whole file in memory, so it is capped at 100 MB— use the chunked flow below for larger files. Returns 201 with the created drive_file. Scope drive.create.

FieldTypeRequiredDescription
namestringyesFile name.
content_typestringyesMIME type.
datastringyesBase64-encoded file contents.
folder_uuidstring | nullnoDestination folder; null or omitted for the root.
curl -X POST https://api.cirrux.co/public_api/v1/drive/files \
  -H "Authorization: Bearer $CIRRUX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "hello.txt",
    "content_type": "text/plain",
    "data": "aGVsbG8gd29ybGQ=",
    "folder_uuid": null
  }'

Download a file

GET/v1/drive/files/{uuid}/download

Returns the decrypted contents as base64, alongside the file metadata. Like the simple upload, this buffers in memory — use the chunked download for large files. Scope drive.read.

{
  "uuid": "a1d9e7c8-…",
  "name": "hello.txt",
  "content_type": "text/plain",
  "size": 11,
  "data": "aGVsbG8gd29ybGQ"
}

Large files (chunked, client-side encryption)

For files above the in-memory cap, upload and download ciphertext directly to storage in chunks. Cirrux hands you a per-file data_key (over TLS) and presigned URLs; your client does the AES-GCM chunk encryption itself, so plaintext never touches Cirrux. All steps use scope drive.create (upload) or drive.read (download).

Upload

POST/v1/drive/uploads

Initiate. Body: name, content_type, plaintext_size_bytes (all required) and optional folder_uuid. Returns file_uuid, data_key, chunk_size_bytes, chunk_count and an s3_upload_id.

POST/v1/drive/uploads/{uuid}/part_urls

Get presigned PUT URLs. Body: part_numbers (array of integers). Upload each encrypted chunk to its URL and keep the returned etag.

POST/v1/drive/uploads/{uuid}/complete

Finalize. Body: parts (array of { part_number, etag }). Flips the file to upload_status: "completed".

POST/v1/drive/uploads/{uuid}/abort

Cancel an in-progress upload and discard the staged parts.

Download

GET/v1/drive/files/{uuid}/download_init

Returns data_key, chunk_size_bytes, chunk_count, ciphertext_size_bytes and a presigned download_url. Fetch ciphertext byte ranges from the URL and decrypt each chunk with the key.

Update a file

POST/v1/drive/files/{uuid}

Send name to rename and/or folder_uuid to move (include the key with a null value to move to the root), or trashed to trash or restore. Omitting a key leaves it unchanged. Returns the updated drive_file. Scope drive.update (setting trashed: true also requires drive.delete).

FieldTypeRequiredDescription
namestringnoNew name.
folder_uuidstring | nullnoNew parent folder; null moves to the root. Present key triggers a move.
trashedbooleannoSet false to restore from trash (scope drive.update); set true to move to trash (scope drive.delete).

Trash a file

POST/v1/drive/files/{uuid}/trash

Moves the file to the trash (recoverable). Returns the file with trashed: true. Scope drive.delete.

Delete a file

DELETE/v1/drive/files/{uuid}

Permanently deletes the file. Returns 204 with an empty body. Scope drive.delete.

Folders

Create a folder

POST/v1/drive/folders

Creates a folder. Returns 201 with the drive_folder. Scope drive.create.

FieldTypeRequiredDescription
namestringyesFolder name.
parent_uuidstring | nullnoParent folder; null or omitted for the root.
colorstringnoOptional folder color, a #RRGGBB hex string.

Retrieve a folder

GET/v1/drive/folders/{uuid}

Returns a single drive_folder. Scope drive.read.

Update a folder

POST/v1/drive/folders/{uuid}

Send name to rename, parent_uuid to move (null moves to the root), color (a #RRGGBB hex string, or blank to clear) to recolor, and/or trashed to trash or restore the folder and its subtree. Returns the updated drive_folder. Scope drive.update (setting trashed: true also requires drive.delete).

FieldTypeRequiredDescription
namestringnoNew name.
parent_uuidstring | nullnoNew parent folder; null moves to the root. Present key triggers a move.
colorstring | nullnoNew color, a #RRGGBB hex string. Blank clears it.
trashedbooleannoSet false to restore, true to trash. Cascades over the whole subtree. Restoring brings back only what was trashed with the folder.

Trash a folder

POST/v1/drive/folders/{uuid}/trash

Moves the folder to the trash. Trashing cascades: every descendant folder and file is trashed too. Scope drive.delete.

Delete a folder

DELETE/v1/drive/folders/{uuid}

Permanently deletes the folder and its entire subtree. Returns 204. Scope drive.delete.

Sharing

Every file and folder already carries your own effective role inline as my_role (see the object definitions above), so you rarely need an extra call to know your access. To see the full picture, who else an item is shared with and its public link, read its sharing settings. These endpoints are read-only. A grant on a folder applies to its whole subtree. Scope drive.read.

Get sharing settings

GET/v1/drive/files/{uuid}/sharing
GET/v1/drive/folders/{uuid}/sharing

Returns a drive_sharing object: the direct grants on the item and its public link, if any. You only need to be able to see the item (viewer or higher).

FieldTypeRequiredDescription
objectstringnoAlways drive_sharing.
resource_typestringnofile or folder.
resource_uuidstringnoThe item these settings belong to.
accessesdrive_access[]noThe direct grants on the item (the grant object is below).
public_linkdrive_public_link | nullnoThe public link, or null when there is none.

The grant object

FieldTypeRequiredDescription
objectstringnoAlways drive_access.
uuidstringnoUnique identifier of the grant.
principal_typestringnoWho it grants to: user or workspace.
principal_user_uuidstring | nullnoThe user, for a user grant.
principal_workspace_uuidstring | nullnoThe workspace, for a workspace grant.
principal_emailstring | nullnoThe principal’s email, when known.
rolestringnoviewer, commenter, editor or manager.
statusstringnopending, accepted or revoked.
created_atstringnoISO-8601 timestamp.
updated_atstringnoISO-8601 timestamp.

The public link object

FieldTypeRequiredDescription
objectstringnoAlways drive_public_link.
uuidstringnoUnique identifier of the link.
resource_typestringnofile or folder.
resource_uuidstringnoThe shared item.
tokenstringnoThe opaque token in the public URL.
urlstringnoThe full public share URL.
statusstringnopending until the copy completes, then ready (or failed).
created_atstringnoISO-8601 timestamp.
updated_atstringnoISO-8601 timestamp.
{
  "object": "drive_sharing",
  "resource_type": "folder",
  "resource_uuid": "b3f1c2a4-…",
  "accesses": [
    {
      "object": "drive_access",
      "uuid": "d4a0b8fb-…",
      "principal_type": "user",
      "principal_user_uuid": "9c2e7a10-…",
      "principal_workspace_uuid": null,
      "principal_email": "sam@example.com",
      "role": "editor",
      "status": "accepted",
      "created_at": "2026-06-19T09:13:40.000Z",
      "updated_at": "2026-06-19T09:13:40.000Z"
    }
  ],
  "public_link": null
}

Thumbnails

Server-rendered image previews, generated once per file and shared across everyone with access. Gate on the file’s has_preview flag: when it is true a preview is ready, otherwise show a generic icon and skip the request (no round-trip). Previews cover images only for now (jpeg, png, gif, webp, tiff, heic); other types are always has_preview: false. Scope drive.read.

Fetch a thumbnail

GET/v1/drive/files/{uuid}/thumbnail

Returns the preview as raw image bytes (Content-Type: image/jpeg or image/png). A trashed file still serves its thumbnail; only a permanently deleted file returns 404.

FieldTypeRequiredDescription
variantstringnoQuery parameter. The size and fit. One variant is available today, 1024_fit (aspect-preserving, longest edge 1024px), which is also the default. An unknown value returns 400 invalid_variant.

The bytes for a given file and variant never change, so responses are cacheable: Cache-Control: private, max-age=31536000, immutable plus an ETag. Send If-None-Match with the stored ETag to get a 304 Not Modified.

curl "https://api.cirrux.co/public_api/v1/drive/files/a1d9e7c8-…/thumbnail?variant=1024_fit" \
  -H "Authorization: Bearer $CIRRUX_TOKEN" --output thumb.jpg

Besides 200, the endpoint can return:

  • 202: eligible but not rendered yet (Retry-After: 1). Rare, since clients gate on has_preview. Show the icon and retry shortly.
  • 204: the file has no preview (unsupported type, over the size cap, or the render failed). Treat it like has_preview: false and stop asking for this version.
  • 304: not modified (your If-None-Match matched). Reuse the cached image.

Fetch thumbnails in a batch

POST/v1/drive/thumbnails/batch

Fetch up to 25 thumbnails in one request (the File Provider batch hint). The call itself returns 200 whenever it was accepted; each item carries its own status, and ready items inline their bytes as base64. A request over 25 uuids returns 400 too_many_uuids.

FieldTypeRequiredDescription
uuidsstring[]yesThe file uuids to fetch, max 25.
variantstringnoThe variant to render. Defaults to 1024_fit.
{
  "object": "list",
  "thumbnails": [
    { "uuid": "a1d9e7c8-…", "status": "ready", "data": "<base64>", "content_type": "image/jpeg", "version": 3 },
    { "uuid": "b2e8f6d9-…", "status": "generating" },
    { "uuid": "c3f9a7ea-…", "status": "no_preview" },
    { "uuid": "d4a0b8fb-…", "status": "not_found" }
  ]
}

Each item’s status mirrors the single-fetch outcome:

  • ready: includes data (base64), content_type and the file version.
  • generating: not rendered yet; re-request it (single fetch or a later batch).
  • no_preview: the file has no preview.
  • not_found: unknown, invisible, or deleted uuid.

Changes (delta sync)

Instead of re-listing the whole tree, poll /v1/drive/changesto get just what changed since your last sync. It returns creates, updates and deletes plus a new cursor to resume from — the same model Google Drive, Dropbox and Microsoft Graph use.

Fetch changes

GET/v1/drive/changes
FieldTypeRequiredDescription
sincestringnoQuery parameter. The sync_token from your last response. Omit it for the first sync, which returns a paginated snapshot of everything.
limitnumbernoQuery parameter. Page size, default 250, max 1000.

Keep calling with the returned sync_token while has_more is true; the first sync’s pages transition seamlessly into the ongoing delta. Persist the token after each page — it’s a durable, crash-safe resume point. Scope drive.read.

curl "https://api.cirrux.co/public_api/v1/drive/changes?since=$SYNC_TOKEN" \
  -H "Authorization: Bearer $CIRRUX_TOKEN"
{
  "object": "list",
  "url": "/v1/drive/changes",
  "as_of": "2026-06-19T12:41:00.000Z",
  "sync_token": "eyJ2IjoxLCJtIjoiZCIsIngiOjEyMDk0MDMzLCJpIjoxMDU5Mn0",
  "has_more": false,
  "changes": [
    {
      "type": "create",
      "resource_type": "file",
      "uuid": "a1d9e7c8-…",
      "resource": { "object": "drive_file", "name": "april-2026.pdf", "trashed": false, … }
    },
    {
      "type": "update",
      "resource_type": "file",
      "uuid": "c7b5a3d1-…",
      "resource": { "object": "drive_file", "name": "draft.pdf", "trashed": true, … }
    },
    {
      "type": "delete",
      "resource_type": "file",
      "uuid": "e9f0a1b2-…",
      "parent_uuid": "b3f1c2a4-…"
    }
  ]
}

Each change is one of:

FieldTypeRequiredDescription
typestringnocreate, update or delete. Apply create and update the same way (upsert by uuid).
resource_typestringnofile or folder.
uuidstringnoThe item that changed.
resourceobjectnoThe full file/folder, on create and update. Omitted on delete.
parent_uuidstring | nullnoOn delete only — where the removed item lived.

Notes:

  • Trash is an update. A trashed item arrives as an update with trashed: true; restoring sends trashed: false. A delete is permanent and carries no resource.
  • Apply in order. Changes are in commit order and a parent always precedes its children. Delivery is at-least-once, so applying must be idempotent; skip a change whose version you already have.
  • Folder cascade. Trashing or deleting a folder emits an explicit change for every descendant, so you never have to walk the tree yourself.
  • Files still uploading are withheld until upload_status is completed.
  • One workspace per feed.The feed only covers the token’s workspace. If you sync several workspaces, use a separate token and cursor for each — each is an independent domain.
StatuserrorWhen
410sync_token_invalidThe token can’t be honored. Discard it and reseed with no since.

Push notifications

Register a device to receive Apple Push Notifications whenever Drive content changes. Designed for native File Provider sync apps that re-enumerate a domain (via the changes feed) when they receive a push. Cirrux pushes to every active subscription whose user can see the changed item. You supply the hex APNs device_token and an apns_client_uuid for a server-managed APNs client; Cirrux mints the provider JWT server-side, so the app ships no .p8 key. Scope drive.read.

Register a subscription

POST/v1/drive/apns_subscriptions
FieldTypeRequiredDescription
apns_client_uuidstringyesServer-managed APNs client configured by Cirrux.
device_tokenstringyesHex APNs device token from pushRegistry(_:didUpdate:for:).
container_identifierstringyesFile Provider container identifier.
domain_identifierstringyesFile Provider domain identifier; must match the authenticated workspace UUID.

Registering again for the same device (matched on apns_client_uuid + container_identifier + domain_identifier) updates it in place. Returns 201; store the returned uuid to unregister later. device_token is never returned.

{
  "object": "drive_apns_subscription",
  "uuid": "b3f1c2a4-0000-0000-0000-000000000000",
  "apns_client_uuid": "b7f3b1dc-0000-0000-0000-000000000000",
  "container_identifier": "NSFileProviderWorkingSetContainerItemIdentifier",
  "domain_identifier": "workspace-abc",
  "created_at": "2026-06-16T12:00:00.000Z",
  "updated_at": "2026-06-16T12:00:00.000Z",
  "unregistered_at": null
}

Unregister a subscription

DELETE/v1/drive/apns_subscriptions/{uuid}

Marks the subscription unregistered; no further pushes are sent. Idempotent. Returns 200 with the subscription, now carrying an unregistered_at timestamp. Cirrux also auto-unregisters a device when APNs reports it gone (410 / BadDeviceToken).

Errors

Drive error codes

In addition to the shared auth errors, Drive endpoints can return:

StatuserrorWhen
404not_foundFile, folder or upload is absent or not visible to you.
403not_authorizedYou can see the item but lack rights to modify it.
422name_takenA live item with that name already exists in the folder.
422missing_required_fieldsA required body field is missing.
422invalid_file_sizeThe declared file size is not valid.
413file_too_largeThe simple upload route’s 100 MB cap was exceeded.
422storage_limit_exceededThe workspace storage pool is full (includes pool_bytes).
422invalid_moveA move would put a folder inside its own subtree.
422invalidA folder color is not a #RRGGBB hex string.
400invalid_bodyThe simple upload data is missing or not valid base64.
400invalid_variantThe requested thumbnail variant is unknown.
400too_many_uuidsA thumbnail batch requested more than 25 uuids.
410sync_token_invalidThe changes cursor can’t be honored; reseed.